diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/ViewComponentTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/ViewComponentTagHelperProducerTest.cs similarity index 81% rename from src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/ViewComponentTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/ViewComponentTagHelperProducerTest.cs index 36326fbd5f2..d75bf5e6e1b 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/ViewComponentTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/ViewComponentTagHelperProducerTest.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; // This is just a basic integration test. There are detailed tests for the VCTH visitor and descriptor factory. -public class ViewComponentTagHelperDescriptorProviderTest +public class ViewComponentTagHelperProducerTest { [Fact] public void DescriptorProvider_FindsVCTH() @@ -24,12 +24,13 @@ public class StringParameterViewComponent var compilation = MvcShim.BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(code)); - var context = new TagHelperDescriptorProviderContext(compilation); - - var provider = new ViewComponentTagHelperDescriptorProvider() + var projectEngine = RazorProjectEngine.CreateEmpty(static b => { - Engine = RazorProjectEngine.CreateEmpty().Engine, - }; + b.Features.Add(new ViewComponentTagHelperProducer.Factory()); + b.Features.Add(new TagHelperDiscoveryService()); + }); + + Assert.True(projectEngine.Engine.TryGetFeature(out ITagHelperDiscoveryService? service)); var expectedDescriptor = TagHelperDescriptorBuilder.CreateViewComponent("__Generated__StringParameterViewComponentTagHelper", TestCompilation.AssemblyName) .TypeName("__Generated__StringParameterViewComponentTagHelper") @@ -55,9 +56,9 @@ public class StringParameterViewComponent .Build(); // Act - provider.Execute(context); + var result = service.GetTagHelpers(compilation); // Assert - Assert.Single(context.Results, d => d.Equals(expectedDescriptor)); + Assert.Single(result, d => d.Equals(expectedDescriptor)); } } diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/ViewComponentTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/ViewComponentTagHelperProducerTest.cs similarity index 81% rename from src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/ViewComponentTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/ViewComponentTagHelperProducerTest.cs index b2be4c917f8..e0e351794eb 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/ViewComponentTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/ViewComponentTagHelperProducerTest.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; // This is just a basic integration test. There are detailed tests for the VCTH visitor and descriptor factory. -public class ViewComponentTagHelperDescriptorProviderTest +public class ViewComponentTagHelperProducerTest { [Fact] public void DescriptorProvider_FindsVCTH() @@ -24,12 +24,13 @@ public class StringParameterViewComponent var compilation = MvcShim.BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(code)); - var context = new TagHelperDescriptorProviderContext(compilation); - - var provider = new ViewComponentTagHelperDescriptorProvider() + var projectEngine = RazorProjectEngine.CreateEmpty(static b => { - Engine = RazorProjectEngine.CreateEmpty().Engine, - }; + b.Features.Add(new ViewComponentTagHelperProducer.Factory()); + b.Features.Add(new TagHelperDiscoveryService()); + }); + + Assert.True(projectEngine.Engine.TryGetFeature(out ITagHelperDiscoveryService? service)); var expectedDescriptor = TagHelperDescriptorBuilder.CreateViewComponent("__Generated__StringParameterViewComponentTagHelper", TestCompilation.AssemblyName) .TypeName("__Generated__StringParameterViewComponentTagHelper") @@ -55,9 +56,9 @@ public class StringParameterViewComponent .Build(); // Act - provider.Execute(context); + var result = service.GetTagHelpers(compilation); // Assert - Assert.Single(context.Results, d => d.Equals(expectedDescriptor)); + Assert.Single(result, d => d.Equals(expectedDescriptor)); } } diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ViewComponentTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ViewComponentTagHelperProducerTest.cs similarity index 81% rename from src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ViewComponentTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ViewComponentTagHelperProducerTest.cs index d3698f54345..b51e057fcf7 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ViewComponentTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ViewComponentTagHelperProducerTest.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; // This is just a basic integration test. There are detailed tests for the VCTH visitor and descriptor factory. -public class ViewComponentTagHelperDescriptorProviderTest +public class ViewComponentTagHelperProducerTest { [Fact] public void DescriptorProvider_FindsVCTH() @@ -24,12 +24,13 @@ public class StringParameterViewComponent var compilation = TestCompilation.Create().AddSyntaxTrees(CSharpSyntaxTree.ParseText(code)); - var context = new TagHelperDescriptorProviderContext(compilation); - - var provider = new ViewComponentTagHelperDescriptorProvider() + var projectEngine = RazorProjectEngine.CreateEmpty(static b => { - Engine = RazorProjectEngine.CreateEmpty().Engine, - }; + b.Features.Add(new ViewComponentTagHelperProducer.Factory()); + b.Features.Add(new TagHelperDiscoveryService()); + }); + + Assert.True(projectEngine.Engine.TryGetFeature(out ITagHelperDiscoveryService? service)); var expectedDescriptor = TagHelperDescriptorBuilder.CreateViewComponent("__Generated__StringParameterViewComponentTagHelper", TestCompilation.AssemblyName) .TypeName("__Generated__StringParameterViewComponentTagHelper") @@ -55,9 +56,9 @@ public class StringParameterViewComponent .Build(); // Act - provider.Execute(context); + var result = service.GetTagHelpers(compilation); // Assert - Assert.Single(context.Results, d => d.Equals(expectedDescriptor)); + Assert.Single(result, d => d.Equals(expectedDescriptor)); } } diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs index b479c94888a..6e493e2172f 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs @@ -89,6 +89,7 @@ private static void AssertDefaultFeatures(RazorProjectEngine engine) feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), + feature => Assert.IsType(feature), feature => Assert.IsType(feature)); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/BindTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/BindTagHelperDescriptorProvider.cs deleted file mode 100644 index dc14b840fbe..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/BindTagHelperDescriptorProvider.cs +++ /dev/null @@ -1,685 +0,0 @@ -// 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 System.Globalization; -using System.Linq; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Components; -using Microsoft.AspNetCore.Razor.PooledObjects; - -namespace Microsoft.CodeAnalysis.Razor; - -// Run after the component tag helper provider, because we need to see the results. -internal sealed class BindTagHelperDescriptorProvider() : TagHelperDescriptorProviderBase(order: 1000) -{ - private static readonly Lazy s_fallbackBindTagHelper = new(CreateFallbackBindTagHelper); - - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) - { - ArgHelper.ThrowIfNull(context); - - // This provider returns tag helper information for 'bind' which doesn't necessarily - // map to any real component. Bind behaviors more like a macro, which can map a single LValue to - // both a 'value' attribute and a 'value changed' attribute. - // - // User types: - // - // - // We generate: - // - // - // This isn't very different from code the user could write themselves - thus the pronouncement - // that @bind is very much like a macro. - // - // A lot of the value that provide in this case is that the associations between the - // elements, and the attributes aren't straightforward. - // - // For instance on we need to listen to 'value' and 'onchange', - // but on - // and so we have a special case for input elements and their type attributes. - // - // Additionally, our mappings tell us about cases like where - // we need to treat the value as an invariant culture value. In general the HTML5 field - // types use invariant culture values when interacting with the DOM, in contrast to - // which is free-form text and is most likely to be - // culture-sensitive. - // - // 4. For components, we have a bit of a special case. We can infer a syntax that matches - // case #2 based on property names. So if a component provides both 'Value' and 'ValueChanged' - // we will turn that into an instance of bind. - // - // So case #1 here is the most general case. Case #2 and #3 are data-driven based on attribute data - // we have. Case #4 is data-driven based on component definitions. - // - // We provide a good set of attributes that map to the HTML dom. This set is user extensible. - var compilation = context.Compilation; - - var bindMethods = compilation.GetTypeByMetadataName(ComponentsApi.BindConverter.FullTypeName); - if (bindMethods == null) - { - // If we can't find BindConverter, then just bail. We won't be able to compile the - // generated code anyway. - return; - } - - if (context.TargetAssembly is { } targetAssembly && - !SymbolEqualityComparer.Default.Equals(targetAssembly, bindMethods.ContainingAssembly)) - { - return; - } - - // Tag Helper definition for case #1. This is the most general case. - context.Results.Add(s_fallbackBindTagHelper.Value); - - var bindElementAttribute = compilation.GetTypeByMetadataName(ComponentsApi.BindElementAttribute.FullTypeName); - var bindInputElementAttribute = compilation.GetTypeByMetadataName(ComponentsApi.BindInputElementAttribute.FullTypeName); - - if (bindElementAttribute == null || bindInputElementAttribute == null) - { - // This won't likely happen, but just in case. - return; - } - - // We want to walk the compilation and its references, not the target symbol. - var collector = new Collector( - compilation, bindElementAttribute, bindInputElementAttribute); - collector.Collect(context, cancellationToken); - } - - private static TagHelperDescriptor CreateFallbackBindTagHelper() - { - using var _ = TagHelperDescriptorBuilder.GetPooledInstance( - TagHelperKind.Bind, "Bind", ComponentsApi.AssemblyName, - out var builder); - - builder.SetTypeName( - fullName: "Microsoft.AspNetCore.Components.Bind", - typeNamespace: "Microsoft.AspNetCore.Components", - typeNameIdentifier: "Bind"); - - builder.CaseSensitive = true; - builder.ClassifyAttributesOnly = true; - builder.SetDocumentation(DocumentationDescriptor.BindTagHelper_Fallback); - - builder.SetMetadata(new BindMetadata() { IsFallback = true }); - - builder.TagMatchingRule(rule => - { - rule.TagName = "*"; - rule.Attribute(attribute => - { - attribute.Name = "@bind-"; - attribute.NameComparison = RequiredAttributeNameComparison.PrefixMatch; - attribute.IsDirectiveAttribute = true; - }); - }); - - builder.BindAttribute(attribute => - { - attribute.SetDocumentation(DocumentationDescriptor.BindTagHelper_Fallback); - - var attributeName = "@bind-..."; - attribute.Name = attributeName; - attribute.AsDictionary("@bind-", typeof(object).FullName); - attribute.IsDirectiveAttribute = true; - - attribute.PropertyName = "Bind"; - - attribute.TypeName = "System.Collections.Generic.Dictionary"; - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "format"; - parameter.PropertyName = "Format"; - parameter.TypeName = typeof(string).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Fallback_Format); - }); - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "event"; - parameter.PropertyName = "Event"; - parameter.TypeName = typeof(string).FullName; - parameter.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.BindTagHelper_Fallback_Event, attributeName)); - }); - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "culture"; - parameter.PropertyName = "Culture"; - parameter.TypeName = typeof(CultureInfo).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Culture); - }); - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "get"; - parameter.PropertyName = "Get"; - parameter.TypeName = typeof(object).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Get); - parameter.BindAttributeGetSet = true; - }); - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "set"; - parameter.PropertyName = "Set"; - parameter.TypeName = typeof(Delegate).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Set); - }); - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "after"; - parameter.PropertyName = "After"; - parameter.TypeName = typeof(Delegate).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_After); - }); - }); - - return builder.Build(); - } - - private class Collector( - Compilation compilation, - INamedTypeSymbol bindElementAttribute, - INamedTypeSymbol bindInputElementAttribute) - : TagHelperCollector(compilation, targetAssembly: null) - { - protected override bool IsCandidateType(INamedTypeSymbol types) - => types.DeclaredAccessibility == Accessibility.Public && - types.Name == "BindAttributes"; - - protected override void Collect(IAssemblySymbol assembly, ICollection results, CancellationToken cancellationToken) - { - // First, collect the initial set of tag helpers from this assembly. This calls - // the Collect(INamedTypeSymbol, ...) overload below for cases #2 & #3. - base.Collect(assembly, results, cancellationToken); - - // Then, for case #4 we look at the tag helpers that were already created corresponding to components - // and pattern match on properties. - using var componentBindTagHelpers = new PooledArrayBuilder(capacity: results.Count); - - foreach (var tagHelper in results) - { - cancellationToken.ThrowIfCancellationRequested(); - - AddComponentBindTagHelpers(tagHelper, ref componentBindTagHelpers.AsRef()); - } - - foreach (var tagHelper in componentBindTagHelpers) - { - results.Add(tagHelper); - } - } - - protected override void Collect( - INamedTypeSymbol type, - ICollection results, - CancellationToken cancellationToken) - { - // Not handling duplicates here for now since we're the primary ones extending this. - // If we see users adding to the set of 'bind' constructs we will want to add deduplication - // and potentially diagnostics. - foreach (var attribute in type.GetAttributes()) - { - var constructorArguments = attribute.ConstructorArguments; - - TagHelperDescriptor? tagHelper = null; - - // For case #2 & #3 we have a whole bunch of attribute entries on BindMethods that we can use - // to data-drive the definitions of these tag helpers. - - // We need to check the constructor argument length here, because this can show up as 0 - // if the language service fails to initialize. This is an invalid case, so skip it. - if (constructorArguments.Length == 4 && SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, bindElementAttribute)) - { - tagHelper = CreateElementBindTagHelper( - typeName: type.GetDefaultDisplayString(), - typeNamespace: type.ContainingNamespace.GetFullName(), - typeNameIdentifier: type.Name, - element: (string?)constructorArguments[0].Value, - typeAttribute: null, - suffix: (string?)constructorArguments[1].Value, - valueAttribute: (string?)constructorArguments[2].Value, - changeAttribute: (string?)constructorArguments[3].Value); - } - else if (constructorArguments.Length == 4 && SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, bindInputElementAttribute)) - { - tagHelper = CreateElementBindTagHelper( - typeName: type.GetDefaultDisplayString(), - typeNamespace: type.ContainingNamespace.GetFullName(), - typeNameIdentifier: type.Name, - element: "input", - typeAttribute: (string?)constructorArguments[0].Value, - suffix: (string?)constructorArguments[1].Value, - valueAttribute: (string?)constructorArguments[2].Value, - changeAttribute: (string?)constructorArguments[3].Value); - } - else if (constructorArguments.Length == 6 && SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, bindInputElementAttribute)) - { - tagHelper = CreateElementBindTagHelper( - typeName: type.GetDefaultDisplayString(), - typeNamespace: type.ContainingNamespace.GetFullName(), - typeNameIdentifier: type.Name, - element: "input", - typeAttribute: (string?)constructorArguments[0].Value, - suffix: (string?)constructorArguments[1].Value, - valueAttribute: (string?)constructorArguments[2].Value, - changeAttribute: (string?)constructorArguments[3].Value, - isInvariantCulture: (bool?)constructorArguments[4].Value ?? false, - format: (string?)constructorArguments[5].Value); - } - - if (tagHelper is not null) - { - results.Add(tagHelper); - } - } - } - - private static TagHelperDescriptor CreateElementBindTagHelper( - string typeName, - string typeNamespace, - string typeNameIdentifier, - string? element, - string? typeAttribute, - string? suffix, - string? valueAttribute, - string? changeAttribute, - bool isInvariantCulture = false, - string? format = null) - { - string name, attributeName, formatName, formatAttributeName, eventName; - - if (suffix is { } s) - { - name = "Bind_" + s; - attributeName = "@bind-" + s; - formatName = "Format_" + s; - formatAttributeName = "format-" + s; - eventName = "Event_" + s; - } - else - { - name = "Bind"; - attributeName = "@bind"; - - suffix = valueAttribute; - formatName = "Format_" + suffix; - formatAttributeName = "format-" + suffix; - eventName = "Event_" + suffix; - } - - using var _ = TagHelperDescriptorBuilder.GetPooledInstance( - TagHelperKind.Bind, name, ComponentsApi.AssemblyName, - out var builder); - - builder.SetTypeName(typeName, typeNamespace, typeNameIdentifier); - - builder.CaseSensitive = true; - builder.ClassifyAttributesOnly = true; - builder.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.BindTagHelper_Element, - valueAttribute, - changeAttribute)); - - var metadata = new BindMetadata.Builder - { - ValueAttribute = valueAttribute, - ChangeAttribute = changeAttribute, - IsInvariantCulture = isInvariantCulture, - Format = format - }; - - if (typeAttribute != null) - { - // For entries that map to the element, we need to be able to know - // the difference between and for which we - // want to use the same attributes. - // - // We provide a tag helper for that should match all input elements, - // but we only want it to be used when a more specific one is used. - // - // Therefore we use this metadata to know which one is more specific when two - // tag helpers match. - metadata.TypeAttribute = typeAttribute; - } - - builder.SetMetadata(metadata.Build()); - - builder.TagMatchingRule(rule => - { - rule.TagName = element; - if (typeAttribute != null) - { - rule.Attribute(a => - { - a.Name = "type"; - a.NameComparison = RequiredAttributeNameComparison.FullMatch; - a.Value = typeAttribute; - a.ValueComparison = RequiredAttributeValueComparison.FullMatch; - }); - } - - rule.Attribute(a => - { - a.Name = attributeName; - a.NameComparison = RequiredAttributeNameComparison.FullMatch; - a.IsDirectiveAttribute = true; - }); - }); - - builder.TagMatchingRule(rule => - { - rule.TagName = element; - if (typeAttribute != null) - { - rule.Attribute(a => - { - a.Name = "type"; - a.NameComparison = RequiredAttributeNameComparison.FullMatch; - a.Value = typeAttribute; - a.ValueComparison = RequiredAttributeValueComparison.FullMatch; - }); - } - - rule.Attribute(a => - { - a.Name = $"{attributeName}:get"; - a.NameComparison = RequiredAttributeNameComparison.FullMatch; - a.IsDirectiveAttribute = true; - }); - - rule.Attribute(a => - { - a.Name = $"{attributeName}:set"; - a.NameComparison = RequiredAttributeNameComparison.FullMatch; - a.IsDirectiveAttribute = true; - }); - }); - - builder.BindAttribute(a => - { - a.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.BindTagHelper_Element, - valueAttribute, - changeAttribute)); - - a.Name = attributeName; - a.TypeName = typeof(object).FullName; - a.IsDirectiveAttribute = true; - a.PropertyName = name; - - a.BindAttributeParameter(parameter => - { - parameter.Name = "format"; - parameter.PropertyName = formatName; - parameter.TypeName = typeof(string).FullName; - parameter.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.BindTagHelper_Element_Format, - attributeName)); - }); - - a.BindAttributeParameter(parameter => - { - parameter.Name = "event"; - parameter.PropertyName = eventName; - parameter.TypeName = typeof(string).FullName; - parameter.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.BindTagHelper_Element_Event, - attributeName)); - }); - - a.BindAttributeParameter(parameter => - { - parameter.Name = "culture"; - parameter.PropertyName = "Culture"; - parameter.TypeName = typeof(CultureInfo).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Culture); - }); - - a.BindAttributeParameter(parameter => - { - parameter.Name = "get"; - parameter.PropertyName = "Get"; - parameter.TypeName = typeof(object).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Get); - parameter.BindAttributeGetSet = true; - }); - - a.BindAttributeParameter(parameter => - { - parameter.Name = "set"; - parameter.PropertyName = "Set"; - parameter.TypeName = typeof(Delegate).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Set); - }); - - a.BindAttributeParameter(parameter => - { - parameter.Name = "after"; - parameter.PropertyName = "After"; - parameter.TypeName = typeof(Delegate).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_After); - }); - }); - - // This is no longer supported. This is just here so we can add a diagnostic later on when this matches. - builder.BindAttribute(attribute => - { - attribute.Name = formatAttributeName; - attribute.TypeName = "System.String"; - attribute.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.BindTagHelper_Element_Format, - attributeName)); - - attribute.PropertyName = formatName; - }); - - return builder.Build(); - } - - private static void AddComponentBindTagHelpers(TagHelperDescriptor tagHelper, ref PooledArrayBuilder results) - { - if (tagHelper.Kind != TagHelperKind.Component) - { - return; - } - - // We want to create a 'bind' tag helper everywhere we see a pair of properties like `Foo`, `FooChanged` - // where `FooChanged` is a delegate and `Foo` is not. - // - // The easiest way to figure this out without a lot of backtracking is to look for `FooChanged` and then - // try to find a matching "Foo". - // - // We also look for a corresponding FooExpression attribute, though its presence is optional. - foreach (var changeAttribute in tagHelper.BoundAttributes) - { - if (!changeAttribute.Name.EndsWith("Changed", StringComparison.Ordinal) || - - // Allow the ValueChanged attribute to be a delegate or EventCallback<>. - // - // We assume that the Delegate or EventCallback<> has a matching type, and the C# compiler will help - // you figure figure it out if you did it wrongly. - (!changeAttribute.IsDelegateProperty() && !changeAttribute.IsEventCallbackProperty())) - { - continue; - } - - BoundAttributeDescriptor? valueAttribute = null; - BoundAttributeDescriptor? expressionAttribute = null; - var valueAttributeName = changeAttribute.Name[..^"Changed".Length]; - var expressionAttributeName = valueAttributeName + "Expression"; - foreach (var attribute in tagHelper.BoundAttributes) - { - if (attribute.Name == valueAttributeName) - { - valueAttribute = attribute; - } - - if (attribute.Name == expressionAttributeName) - { - expressionAttribute = attribute; - } - - if (valueAttribute != null && expressionAttribute != null) - { - // We found both, so we can stop looking now - break; - } - } - - if (valueAttribute == null) - { - // No matching attribute found. - continue; - } - - using var _ = TagHelperDescriptorBuilder.GetPooledInstance( - TagHelperKind.Bind, tagHelper.Name, tagHelper.AssemblyName, - out var builder); - - builder.SetTypeName(tagHelper.TypeNameObject); - - builder.DisplayName = tagHelper.DisplayName; - builder.CaseSensitive = true; - builder.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.BindTagHelper_Component, - valueAttribute.Name, - changeAttribute.Name)); - - var metadata = new BindMetadata.Builder - { - ValueAttribute = valueAttribute.Name, - ChangeAttribute = changeAttribute.Name - }; - - if (expressionAttribute != null) - { - metadata.ExpressionAttribute = expressionAttribute.Name; - } - - // Match the component and attribute name - builder.TagMatchingRule(rule => - { - rule.TagName = tagHelper.TagMatchingRules.Single().TagName; - rule.Attribute(attribute => - { - attribute.Name = "@bind-" + valueAttribute.Name; - attribute.NameComparison = RequiredAttributeNameComparison.FullMatch; - attribute.IsDirectiveAttribute = true; - }); - }); - - builder.TagMatchingRule(rule => - { - rule.TagName = tagHelper.TagMatchingRules.Single().TagName; - rule.Attribute(attribute => - { - attribute.Name = "@bind-" + valueAttribute.Name + ":get"; - attribute.NameComparison = RequiredAttributeNameComparison.FullMatch; - attribute.IsDirectiveAttribute = true; - }); - rule.Attribute(attribute => - { - attribute.Name = "@bind-" + valueAttribute.Name + ":set"; - attribute.NameComparison = RequiredAttributeNameComparison.FullMatch; - attribute.IsDirectiveAttribute = true; - }); - }); - - builder.BindAttribute(attribute => - { - attribute.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.BindTagHelper_Component, - valueAttribute.Name, - changeAttribute.Name)); - - attribute.Name = "@bind-" + valueAttribute.Name; - attribute.TypeName = changeAttribute.TypeName; - attribute.IsEnum = valueAttribute.IsEnum; - attribute.ContainingType = valueAttribute.ContainingType; - attribute.IsDirectiveAttribute = true; - attribute.PropertyName = valueAttribute.PropertyName; - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "get"; - parameter.PropertyName = "Get"; - parameter.TypeName = typeof(object).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Get); - parameter.BindAttributeGetSet = true; - }); - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "set"; - parameter.PropertyName = "Set"; - parameter.TypeName = typeof(Delegate).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Set); - }); - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "after"; - parameter.PropertyName = "After"; - parameter.TypeName = typeof(Delegate).FullName; - parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_After); - }); - }); - - if (tagHelper.IsFullyQualifiedNameMatch) - { - builder.IsFullyQualifiedNameMatch = true; - } - - builder.SetMetadata(metadata.Build()); - - results.Add(builder.Build()); - } - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilationTagHelperFeature.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilationTagHelperFeature.cs index 40f8909b4d3..7d75cd3d507 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilationTagHelperFeature.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilationTagHelperFeature.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.CSharp; @@ -12,7 +13,7 @@ namespace Microsoft.CodeAnalysis.Razor; public sealed class CompilationTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature { - private ImmutableArray _providers; + private ITagHelperDiscoveryService? _discoveryService; private IMetadataReferenceFeature? _referenceFeature; public TagHelperCollection GetTagHelpers(CancellationToken cancellationToken = default) @@ -23,21 +24,15 @@ public TagHelperCollection GetTagHelpers(CancellationToken cancellationToken = d return []; } - using var builder = new TagHelperCollection.Builder(); - var context = new TagHelperDescriptorProviderContext(compilation, builder); + Assumed.NotNull(_discoveryService); - foreach (var provider in _providers) - { - provider.Execute(context, cancellationToken); - } - - return builder.ToCollection(); + return _discoveryService.GetTagHelpers(compilation, cancellationToken); } protected override void OnInitialized() { _referenceFeature = Engine.GetFeatures().FirstOrDefault(); - _providers = Engine.GetFeatures().OrderByAsArray(static f => f.Order); + _discoveryService = GetRequiredFeature(); } internal static bool IsValidCompilation(Compilation compilation) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilerFeatures.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilerFeatures.cs index c774727e7f2..910d9e4348e 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilerFeatures.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilerFeatures.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; namespace Microsoft.CodeAnalysis.Razor; @@ -19,25 +18,22 @@ public static class CompilerFeatures /// The . public static void Register(RazorProjectEngineBuilder builder) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + ArgHelper.ThrowIfNull(builder); if (builder.Configuration.LanguageVersion >= RazorLanguageVersion.Version_3_0) { - builder.Features.Add(new BindTagHelperDescriptorProvider()); - builder.Features.Add(new ComponentTagHelperDescriptorProvider()); - builder.Features.Add(new EventHandlerTagHelperDescriptorProvider()); - builder.Features.Add(new RefTagHelperDescriptorProvider()); - builder.Features.Add(new KeyTagHelperDescriptorProvider()); - builder.Features.Add(new SplatTagHelperDescriptorProvider()); + builder.Features.Add(new BindTagHelperProducer.Factory()); + builder.Features.Add(new ComponentTagHelperProducer.Factory()); + builder.Features.Add(new EventHandlerTagHelperProducer.Factory()); + builder.Features.Add(new RefTagHelperProducer.Factory()); + builder.Features.Add(new KeyTagHelperProducer.Factory()); + builder.Features.Add(new SplatTagHelperProducer.Factory()); } if (builder.Configuration.LanguageVersion >= RazorLanguageVersion.Version_8_0) { - builder.Features.Add(new RenderModeTagHelperDescriptorProvider()); - builder.Features.Add(new FormNameTagHelperDescriptorProvider()); + builder.Features.Add(new RenderModeTagHelperProducer.Factory()); + builder.Features.Add(new FormNameTagHelperProducer.Factory()); } } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs deleted file mode 100644 index ec3b5ec2fc3..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs +++ /dev/null @@ -1,759 +0,0 @@ -// 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 System.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Components; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.CSharp; - -namespace Microsoft.CodeAnalysis.Razor; - -internal sealed class ComponentTagHelperDescriptorProvider : TagHelperDescriptorProviderBase -{ - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) - { - ArgHelper.ThrowIfNull(context); - - var compilation = context.Compilation; - var targetAssembly = context.TargetAssembly; - - var collector = new Collector(compilation, targetAssembly); - collector.Collect(context, cancellationToken); - } - - private sealed class Collector( - Compilation compilation, - IAssemblySymbol? targetAssembly) - : TagHelperCollector(compilation, targetAssembly) - { - protected override bool IsCandidateType(INamedTypeSymbol type) - => ComponentDetectionConventions.IsComponent(type, ComponentsApi.IComponent.MetadataName); - - protected override void Collect( - INamedTypeSymbol type, - ICollection results, - CancellationToken cancellationToken) - { - // Components have very simple matching rules. - // 1. The type name (short) matches the tag name. - // 2. The fully qualified name matches the tag name. - - // First, compute the relevant properties for this type so that we - // don't need to compute them twice. - var properties = GetProperties(type); - - var shortNameMatchingDescriptor = CreateShortNameMatchingDescriptor(type, properties); - results.Add(shortNameMatchingDescriptor); - - // If the component is in the global namespace, skip adding this descriptor which will be the same as the short name one. - TagHelperDescriptor? fullyQualifiedNameMatchingDescriptor = null; - if (!type.ContainingNamespace.IsGlobalNamespace) - { - fullyQualifiedNameMatchingDescriptor = CreateFullyQualifiedNameMatchingDescriptor(type, properties); - results.Add(fullyQualifiedNameMatchingDescriptor); - } - - foreach (var childContent in shortNameMatchingDescriptor.GetChildContentProperties()) - { - // Synthesize a separate tag helper for each child content property that's declared. - results.Add(CreateChildContentDescriptor(shortNameMatchingDescriptor, childContent)); - if (fullyQualifiedNameMatchingDescriptor is not null) - { - results.Add(CreateChildContentDescriptor(fullyQualifiedNameMatchingDescriptor, childContent)); - } - } - } - - private static TagHelperDescriptor CreateShortNameMatchingDescriptor( - INamedTypeSymbol type, - ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties) - => CreateNameMatchingDescriptor(type, properties, fullyQualified: false); - - private static TagHelperDescriptor CreateFullyQualifiedNameMatchingDescriptor( - INamedTypeSymbol type, - ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties) - => CreateNameMatchingDescriptor(type, properties, fullyQualified: true); - - private static TagHelperDescriptor CreateNameMatchingDescriptor( - INamedTypeSymbol type, - ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties, - bool fullyQualified) - { - var typeName = TypeNameObject.From(type); - var assemblyName = type.ContainingAssembly.Identity.Name; - - using var _ = TagHelperDescriptorBuilder.GetPooledInstance( - TagHelperKind.Component, typeName.FullName.AssumeNotNull(), assemblyName, out var builder); - - builder.RuntimeKind = RuntimeKind.IComponent; - builder.SetTypeName(typeName); - - var metadata = new ComponentMetadata.Builder(); - - builder.CaseSensitive = true; - - if (fullyQualified) - { - var fullName = type.ContainingNamespace.IsGlobalNamespace - ? type.Name - : $"{type.ContainingNamespace.GetFullName()}.{type.Name}"; - - builder.TagMatchingRule(r => - { - r.TagName = fullName; - }); - - builder.IsFullyQualifiedNameMatch = true; - } - else - { - builder.TagMatchingRule(r => - { - r.TagName = type.Name; - }); - } - - if (type.IsGenericType) - { - metadata.IsGeneric = true; - - using var cascadeGenericTypeAttributes = new PooledHashSet(StringComparer.Ordinal); - - foreach (var attribute in type.GetAttributes()) - { - if (attribute.HasFullName(ComponentsApi.CascadingTypeParameterAttribute.MetadataName) && - attribute.ConstructorArguments.FirstOrDefault() is { Value: string value }) - { - cascadeGenericTypeAttributes.Add(value); - } - } - - foreach (var typeArgument in type.TypeArguments) - { - if (typeArgument is ITypeParameterSymbol typeParameter) - { - var cascade = cascadeGenericTypeAttributes.Contains(typeParameter.Name); - CreateTypeParameterProperty(builder, typeParameter, cascade); - } - } - } - - if (HasRenderModeDirective(type)) - { - metadata.HasRenderModeDirective = true; - } - - var xml = type.GetDocumentationCommentXml(); - if (!string.IsNullOrEmpty(xml)) - { - builder.SetDocumentation(xml); - } - - foreach (var (property, kind) in properties) - { - if (kind == PropertyKind.Ignored) - { - continue; - } - - CreateProperty(builder, type, property, kind); - } - - if (builder.BoundAttributes.Any(static a => a.IsParameterizedChildContentProperty()) && - !builder.BoundAttributes.Any(static a => string.Equals(a.Name, ComponentHelpers.ChildContent.ParameterAttributeName, StringComparison.OrdinalIgnoreCase))) - { - // If we have any parameterized child content parameters, synthesize a 'Context' parameter to be - // able to set the variable name (for all child content). If the developer defined a 'Context' parameter - // already, then theirs wins. - CreateContextParameter(builder, childContentName: null); - } - - builder.SetMetadata(metadata.Build()); - - return builder.Build(); - } - - private static void CreateProperty(TagHelperDescriptorBuilder builder, INamedTypeSymbol containingSymbol, IPropertySymbol property, PropertyKind kind) - { - builder.BindAttribute(pb => - { - var builder = new PropertyMetadata.Builder(); - - pb.Name = property.Name; - pb.ContainingType = containingSymbol.GetFullName(); - pb.TypeName = property.Type.GetFullName(); - pb.PropertyName = property.Name; - pb.IsEditorRequired = property.GetAttributes().Any( - static a => a.HasFullName("Microsoft.AspNetCore.Components.EditorRequiredAttribute")); - - pb.CaseSensitive = false; - - builder.GloballyQualifiedTypeName = property.Type.GetGloballyQualifiedFullName(); - - if (kind == PropertyKind.Enum) - { - pb.IsEnum = true; - } - else if (kind == PropertyKind.ChildContent) - { - builder.IsChildContent = true; - } - else if (kind == PropertyKind.EventCallback) - { - builder.IsEventCallback = true; - } - else if (kind == PropertyKind.Delegate) - { - builder.IsDelegateSignature = true; - builder.IsDelegateWithAwaitableResult = IsAwaitable(property); - } - - if (HasTypeParameter(property.Type)) - { - builder.IsGenericTyped = true; - } - - if (property.SetMethod.AssumeNotNull().IsInitOnly) - { - builder.IsInitOnlyProperty = true; - } - - pb.SetMetadata(builder.Build()); - - var xml = property.GetDocumentationCommentXml(); - if (!string.IsNullOrEmpty(xml)) - { - pb.SetDocumentation(xml); - } - }); - - static bool HasTypeParameter(ITypeSymbol type) - { - if (type is ITypeParameterSymbol) - { - return true; - } - - // We need to check for cases like: - // [Parameter] public List MyProperty { get; set; } - // AND - // [Parameter] public List MyProperty { get; set; } - // - // We need to inspect the type arguments to tell the difference between a property that - // uses the containing class' type parameter(s) and a vanilla usage of generic types like - // List<> and Dictionary<,> - // - // Since we need to handle cases like RenderFragment>, this check must be recursive. - if (type is INamedTypeSymbol namedType && namedType.IsGenericType) - { - foreach (var typeArgument in namedType.TypeArguments) - { - if (HasTypeParameter(typeArgument)) - { - return true; - } - } - - // Another case to handle - if the type being inspected is a nested type - // inside a generic containing class. The common usage for this would be a case - // where a generic templated component defines a 'context' nested class. - if (namedType.ContainingType != null && HasTypeParameter(namedType.ContainingType)) - { - return true; - } - } - // Also check for cases like: - // [Parameter] public T[] MyProperty { get; set; } - else if (type is IArrayTypeSymbol array && HasTypeParameter(array.ElementType)) - { - return true; - } - - return false; - } - } - - private static bool IsAwaitable(IPropertySymbol prop) - { - var methodSymbol = ((INamedTypeSymbol)prop.Type).DelegateInvokeMethod.AssumeNotNull(); - if (methodSymbol.ReturnsVoid) - { - return false; - } - - var members = methodSymbol.ReturnType.GetMembers(); - foreach (var candidate in members) - { - if (candidate is not IMethodSymbol method || !string.Equals(candidate.Name, "GetAwaiter", StringComparison.Ordinal)) - { - continue; - } - - if (!VerifyGetAwaiter(method)) - { - continue; - } - - return true; - } - - return methodSymbol.IsAsync; - - static bool VerifyGetAwaiter(IMethodSymbol getAwaiter) - { - var returnType = getAwaiter.ReturnType; - if (returnType == null) - { - return false; - } - - var foundIsCompleted = false; - var foundOnCompleted = false; - var foundGetResult = false; - - foreach (var member in returnType.GetMembers()) - { - if (!foundIsCompleted && - member is IPropertySymbol property && - IsProperty_IsCompleted(property)) - { - foundIsCompleted = true; - } - - if (!(foundOnCompleted && foundGetResult) && member is IMethodSymbol method) - { - if (IsMethod_OnCompleted(method)) - { - foundOnCompleted = true; - } - else if (IsMethod_GetResult(method)) - { - foundGetResult = true; - } - } - - if (foundIsCompleted && foundOnCompleted && foundGetResult) - { - return true; - } - } - - return false; - - static bool IsProperty_IsCompleted(IPropertySymbol property) - { - return property is - { - Name: WellKnownMemberNames.IsCompleted, - Type.SpecialType: SpecialType.System_Boolean, - GetMethod: not null - }; - } - - static bool IsMethod_OnCompleted(IMethodSymbol method) - { - return method is - { - Name: WellKnownMemberNames.OnCompleted, - ReturnsVoid: true, - Parameters: [{ Type.TypeKind: TypeKind.Delegate }] - }; - } - - static bool IsMethod_GetResult(IMethodSymbol method) - { - return method is - { - Name: WellKnownMemberNames.GetResult, - Parameters: [] - }; - } - } - } - - private static void CreateTypeParameterProperty(TagHelperDescriptorBuilder builder, ITypeParameterSymbol typeParameter, bool cascade) - { - builder.BindAttribute(pb => - { - pb.DisplayName = typeParameter.Name; - pb.Name = typeParameter.Name; - pb.TypeName = typeof(Type).FullName; - pb.PropertyName = typeParameter.Name; - - var metadata = new TypeParameterMetadata.Builder - { - IsCascading = cascade - }; - - // Type constraints (like "Image" or "Foo") are stored independently of - // things like constructor constraints and not null constraints in the - // type parameter so we create a single string representation of all the constraints - // here. - using var constraints = new PooledList(); - - // CS0449: The 'class', 'struct', 'unmanaged', 'notnull', and 'default' constraints - // cannot be combined or duplicated, and must be specified first in the constraints list. - if (typeParameter.HasReferenceTypeConstraint) - { - constraints.Add("class"); - } - - if (typeParameter.HasNotNullConstraint) - { - constraints.Add("notnull"); - } - - if (typeParameter.HasUnmanagedTypeConstraint) - { - constraints.Add("unmanaged"); - } - else if (typeParameter.HasValueTypeConstraint) - { - // `HasValueTypeConstraint` is also true when `unmanaged` constraint is present. - constraints.Add("struct"); - } - - foreach (var constraintType in typeParameter.ConstraintTypes) - { - constraints.Add(constraintType.GetGloballyQualifiedFullName()); - } - - // CS0401: The new() constraint must be the last constraint specified. - if (typeParameter.HasConstructorConstraint) - { - constraints.Add("new()"); - } - - if (TryGetWhereClauseText(typeParameter, constraints, out var whereClauseText)) - { - metadata.Constraints = whereClauseText; - } - - // Collect attributes that should be propagated to the type inference method. - using var _ = StringBuilderPool.GetPooledObject(out var withAttributes); - foreach (var attribute in typeParameter.GetAttributes()) - { - if (attribute.HasFullName("System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute")) - { - Debug.Assert(attribute.AttributeClass != null); - - if (withAttributes.Length > 0) - { - withAttributes.Append(", "); - } - else - { - withAttributes.Append('['); - } - - withAttributes.Append(attribute.AttributeClass.GetGloballyQualifiedFullName()); - withAttributes.Append('('); - - var first = true; - foreach (var arg in attribute.ConstructorArguments) - { - if (first) - { - first = false; - } - else - { - withAttributes.Append(", "); - } - - if (arg.Kind == TypedConstantKind.Enum) - { - withAttributes.Append("unchecked(("); - withAttributes.Append(arg.Type!.GetGloballyQualifiedFullName()); - withAttributes.Append(')'); - withAttributes.Append(CSharp.SymbolDisplay.FormatPrimitive(arg.Value!, quoteStrings: true, useHexadecimalNumbers: true)); - withAttributes.Append(')'); - } - else - { - Debug.Assert(false, $"Need to add support for '{arg.Kind}' and make sure the output is 'global::' prefixed."); - withAttributes.Append(arg.ToCSharpString()); - } - } - - withAttributes.Append(')'); - } - } - - if (withAttributes.Length > 0) - { - withAttributes.Append("] "); - withAttributes.Append(typeParameter.Name); - metadata.NameWithAttributes = withAttributes.ToString(); - } - - pb.SetMetadata(metadata.Build()); - - pb.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.ComponentTypeParameter, - typeParameter.Name, - builder.Name)); - }); - - static bool TryGetWhereClauseText(ITypeParameterSymbol typeParameter, PooledList constraints, [NotNullWhen(true)] out string? constraintsText) - { - if (constraints.Count == 0) - { - constraintsText = null; - return false; - } - - using var _ = StringBuilderPool.GetPooledObject(out var builder); - - builder.Append("where "); - builder.Append(typeParameter.Name); - builder.Append(" : "); - - var addComma = false; - - foreach (var item in constraints) - { - if (addComma) - { - builder.Append(", "); - } - else - { - addComma = true; - } - - builder.Append(item); - } - - constraintsText = builder.ToString(); - return true; - } - } - - private static TagHelperDescriptor CreateChildContentDescriptor(TagHelperDescriptor component, BoundAttributeDescriptor attribute) - { - var typeName = component.TypeName + "." + attribute.Name; - var assemblyName = component.AssemblyName; - - using var _ = TagHelperDescriptorBuilder.GetPooledInstance( - TagHelperKind.ChildContent, typeName, assemblyName, - out var builder); - - builder.SetTypeName(typeName, component.TypeNamespace, component.TypeNameIdentifier); - - builder.CaseSensitive = true; - - var xml = attribute.Documentation; - if (!string.IsNullOrEmpty(xml)) - { - builder.SetDocumentation(xml); - } - - // Child content matches the property name, but only as a direct child of the component. - builder.TagMatchingRule(r => - { - r.TagName = attribute.Name; - r.ParentTag = component.TagMatchingRules[0].TagName; - }); - - if (attribute.IsParameterizedChildContentProperty()) - { - // For child content attributes with a parameter, synthesize an attribute that allows you to name - // the parameter. - CreateContextParameter(builder, attribute.Name); - } - - if (component.IsFullyQualifiedNameMatch) - { - builder.IsFullyQualifiedNameMatch = true; - } - - var descriptor = builder.Build(); - - return descriptor; - } - - private static void CreateContextParameter(TagHelperDescriptorBuilder builder, string? childContentName) - { - builder.BindAttribute(b => - { - b.Name = ComponentHelpers.ChildContent.ParameterAttributeName; - b.TypeName = typeof(string).FullName; - b.PropertyName = b.Name; - b.SetMetadata(ChildContentParameterMetadata.Default); - - var documentation = childContentName == null - ? DocumentationDescriptor.ChildContentParameterName_TopLevel - : DocumentationDescriptor.From(DocumentationId.ChildContentParameterName, childContentName); - - b.SetDocumentation(documentation); - }); - } - - // Does a walk up the inheritance chain to determine the set of parameters by using - // a dictionary keyed on property name. - // - // We consider parameters to be defined by properties satisfying all of the following: - // - are public - // - are visible (not shadowed) - // - have the [Parameter] attribute - // - have a setter, even if private - // - are not indexers - private static ImmutableArray<(IPropertySymbol property, PropertyKind kind)> GetProperties(INamedTypeSymbol type) - { - using var names = new PooledHashSet(StringComparer.Ordinal); - using var results = new PooledArrayBuilder<(IPropertySymbol, PropertyKind)>(); - - var currentType = type; - do - { - if (currentType.HasFullName(ComponentsApi.ComponentBase.MetadataName)) - { - // The ComponentBase base class doesn't have any [Parameter]. - // Bail out now to avoid walking through its many members, plus the members - // of the System.Object base class. - break; - } - - foreach (var member in currentType.GetMembers()) - { - if (member is not IPropertySymbol property) - { - // Not a property - continue; - } - - if (names.Contains(property.Name)) - { - // Not visible - continue; - } - - var kind = PropertyKind.Default; - if (property.DeclaredAccessibility != Accessibility.Public) - { - // Not public - kind = PropertyKind.Ignored; - } - - if (property.Parameters.Length != 0) - { - // Indexer - kind = PropertyKind.Ignored; - } - - if (property.SetMethod == null) - { - // No setter - kind = PropertyKind.Ignored; - } - else if (property.SetMethod.DeclaredAccessibility != Accessibility.Public) - { - // No public setter - kind = PropertyKind.Ignored; - } - - if (property.IsStatic) - { - kind = PropertyKind.Ignored; - } - - if (!property.GetAttributes().Any(static a => a.HasFullName(ComponentsApi.ParameterAttribute.MetadataName))) - { - if (property.IsOverride) - { - // This property does not contain [Parameter] attribute but it was overridden. Don't ignore it for now. - // We can ignore it if the base class does not contains a [Parameter] as well. - continue; - } - - // Does not have [Parameter] - kind = PropertyKind.Ignored; - } - - if (kind == PropertyKind.Default) - { - kind = property switch - { - var p when IsEnum(p) => PropertyKind.Enum, - var p when IsRenderFragment(p) => PropertyKind.ChildContent, - var p when IsEventCallback(p) => PropertyKind.EventCallback, - var p when IsDelegate(p) => PropertyKind.Delegate, - _ => PropertyKind.Default - }; - } - - names.Add(property.Name); - results.Add((property, kind)); - } - - currentType = currentType.BaseType; - } - while (currentType != null); - - return results.ToImmutableAndClear(); - - static bool IsEnum(IPropertySymbol property) - { - return property.Type.TypeKind == TypeKind.Enum; - } - - static bool IsRenderFragment(IPropertySymbol property) - { - return property.Type.HasFullName(ComponentsApi.RenderFragment.MetadataName) || - (property.Type is INamedTypeSymbol { IsGenericType: true } namedType && - namedType.ConstructedFrom.HasFullName(ComponentsApi.RenderFragmentOfT.DisplayName)); - } - - static bool IsEventCallback(IPropertySymbol property) - { - return property.Type.HasFullName(ComponentsApi.EventCallback.MetadataName) || - (property.Type is INamedTypeSymbol { IsGenericType: true } namedType && - namedType.ConstructedFrom.HasFullName(ComponentsApi.EventCallbackOfT.DisplayName)); - } - - static bool IsDelegate(IPropertySymbol property) - { - return property.Type.TypeKind == TypeKind.Delegate; - } - } - - private static bool HasRenderModeDirective(INamedTypeSymbol type) - { - var attributes = type.GetAttributes(); - foreach (var attribute in attributes) - { - var attributeClass = attribute.AttributeClass; - while (attributeClass is not null) - { - if (attributeClass.HasFullName(ComponentsApi.RenderModeAttribute.FullTypeName)) - { - return true; - } - - attributeClass = attributeClass.BaseType; - } - } - return false; - } - - private enum PropertyKind - { - Ignored, - Default, - Enum, - ChildContent, - Delegate, - EventCallback, - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/DefaultTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/DefaultTagHelperDescriptorProvider.cs deleted file mode 100644 index 35dfbd5fcc9..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/DefaultTagHelperDescriptorProvider.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; - -namespace Microsoft.CodeAnalysis.Razor; - -public sealed class DefaultTagHelperDescriptorProvider : TagHelperDescriptorProviderBase -{ - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) - { - ArgHelper.ThrowIfNull(context); - - var compilation = context.Compilation; - - var iTagHelperType = compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper); - if (iTagHelperType == null || iTagHelperType.TypeKind == TypeKind.Error) - { - // Could not find attributes we care about in the compilation. Nothing to do. - return; - } - - var targetAssembly = context.TargetAssembly; - var factory = new DefaultTagHelperDescriptorFactory(context.IncludeDocumentation, context.ExcludeHidden); - var collector = new Collector(compilation, targetAssembly, factory, iTagHelperType); - collector.Collect(context, cancellationToken); - } - - private class Collector( - Compilation compilation, - IAssemblySymbol? targetAssembly, - DefaultTagHelperDescriptorFactory factory, - INamedTypeSymbol iTagHelperType) - : TagHelperCollector(compilation, targetAssembly) - { - private readonly DefaultTagHelperDescriptorFactory _factory = factory; - private readonly INamedTypeSymbol _iTagHelperType = iTagHelperType; - - protected override bool IsCandidateType(INamedTypeSymbol type) - => type.IsTagHelper(_iTagHelperType); - - protected override void Collect( - INamedTypeSymbol type, - ICollection results, - CancellationToken cancellationToken) - { - var descriptor = _factory.CreateDescriptor(type); - - if (descriptor != null) - { - results.Add(descriptor); - } - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/EventHandlerTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/EventHandlerTagHelperDescriptorProvider.cs deleted file mode 100644 index 828c18ab326..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/EventHandlerTagHelperDescriptorProvider.cs +++ /dev/null @@ -1,253 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Components; - -namespace Microsoft.CodeAnalysis.Razor; - -internal sealed class EventHandlerTagHelperDescriptorProvider : TagHelperDescriptorProviderBase -{ - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) - { - ArgHelper.ThrowIfNull(context); - - var compilation = context.Compilation; - - if (compilation.GetTypeByMetadataName(ComponentsApi.EventHandlerAttribute.FullTypeName) is not INamedTypeSymbol eventHandlerAttribute) - { - // If we can't find EventHandlerAttribute, then just bail. We won't discover anything. - return; - } - - var targetAssembly = context.TargetAssembly; - - var collector = new Collector(compilation, targetAssembly, eventHandlerAttribute); - collector.Collect(context, cancellationToken); - } - - private class Collector( - Compilation compilation, - IAssemblySymbol? targetAssembly, - INamedTypeSymbol eventHandlerAttribute) - : TagHelperCollector(compilation, targetAssembly) - { - private readonly INamedTypeSymbol _eventHandlerAttribute = eventHandlerAttribute; - - protected override bool IsCandidateType(INamedTypeSymbol type) - => type.DeclaredAccessibility == Accessibility.Public && - type.Name == "EventHandlers"; - - protected override void Collect( - INamedTypeSymbol type, - ICollection results, - CancellationToken cancellationToken) - { - // Not handling duplicates here for now since we're the primary ones extending this. - // If we see users adding to the set of event handler constructs we will want to add deduplication - // and potentially diagnostics. - foreach (var attribute in type.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _eventHandlerAttribute)) - { - if (!AttributeArgs.TryGet(attribute, out var args)) - { - // If this occurs, the [EventHandler] was defined incorrectly, so we can't create a tag helper. - continue; - } - - var typeName = type.GetDefaultDisplayString(); - var namespaceName = type.ContainingNamespace.GetFullName(); - results.Add(CreateTagHelper(typeName, namespaceName, type.Name, args)); - } - } - } - - private readonly record struct AttributeArgs( - string Attribute, - INamedTypeSymbol EventArgsType, - bool EnableStopPropagation = false, - bool EnablePreventDefault = false) - { - public static bool TryGet(AttributeData attribute, out AttributeArgs args) - { - // EventHandlerAttribute has two constructors: - // - // - EventHandlerAttribute(string attributeName, Type eventArgsType); - // - EventHandlerAttribute(string attributeName, Type eventArgsType, bool enableStopPropagation, bool enablePreventDefault); - - var arguments = attribute.ConstructorArguments; - - return TryGetFromTwoArguments(arguments, out args) || - TryGetFromFourArguments(arguments, out args); - - static bool TryGetFromTwoArguments(ImmutableArray arguments, out AttributeArgs args) - { - // Ctor 1: EventHandlerAttribute(string attributeName, Type eventArgsType); - - if (arguments is [ - { Value: string attributeName }, - { Value: INamedTypeSymbol eventArgsType }]) - { - args = new(attributeName, eventArgsType); - return true; - } - - args = default; - return false; - } - - static bool TryGetFromFourArguments(ImmutableArray arguments, out AttributeArgs args) - { - // Ctor 2: EventHandlerAttribute(string attributeName, Type eventArgsType, bool enableStopPropagation, bool enablePreventDefault); - - // TODO: The enablePreventDefault and enableStopPropagation arguments are incorrectly swapped! - // However, they have been that way since the 4-argument constructor variant was introduced - // in https://github.com/dotnet/razor/commit/7635bba6ef2d3e6798d0846ceb96da6d5908e1b0. - // Fixing this is tracked be https://github.com/dotnet/razor/issues/10497 - - if (arguments is [ - { Value: string attributeName }, - { Value: INamedTypeSymbol eventArgsType }, - { Value: bool enablePreventDefault }, - { Value: bool enableStopPropagation }]) - { - args = new(attributeName, eventArgsType, enableStopPropagation, enablePreventDefault); - return true; - } - - args = default; - return false; - } - } - } - - private static TagHelperDescriptor CreateTagHelper( - string typeName, - string typeNamespace, - string typeNameIdentifier, - AttributeArgs args) - { - var (attribute, eventArgsType, enableStopPropagation, enablePreventDefault) = args; - - var attributeName = "@" + attribute; - var eventArgType = eventArgsType.GetDefaultDisplayString(); - using var _ = TagHelperDescriptorBuilder.GetPooledInstance( - TagHelperKind.EventHandler, attribute, ComponentsApi.AssemblyName, - out var builder); - - builder.SetTypeName(typeName, typeNamespace, typeNameIdentifier); - - builder.CaseSensitive = true; - builder.ClassifyAttributesOnly = true; - builder.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.EventHandlerTagHelper, - attributeName, - eventArgType)); - - builder.SetMetadata(new EventHandlerMetadata() - { - EventArgsType = eventArgType - }); - - builder.TagMatchingRule(rule => - { - rule.TagName = "*"; - - rule.Attribute(a => - { - a.Name = attributeName; - a.NameComparison = RequiredAttributeNameComparison.FullMatch; - a.IsDirectiveAttribute = true; - }); - }); - - if (enablePreventDefault) - { - builder.TagMatchingRule(rule => - { - rule.TagName = "*"; - - rule.Attribute(a => - { - a.Name = attributeName + ":preventDefault"; - a.NameComparison = RequiredAttributeNameComparison.FullMatch; - a.IsDirectiveAttribute = true; - }); - }); - } - - if (enableStopPropagation) - { - builder.TagMatchingRule(rule => - { - rule.TagName = "*"; - - rule.Attribute(a => - { - a.Name = attributeName + ":stopPropagation"; - a.NameComparison = RequiredAttributeNameComparison.FullMatch; - a.IsDirectiveAttribute = true; - }); - }); - } - - builder.BindAttribute(a => - { - a.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.EventHandlerTagHelper, - attributeName, - eventArgType)); - - a.Name = attributeName; - - // We want event handler directive attributes to default to C# context. - a.TypeName = $"Microsoft.AspNetCore.Components.EventCallback<{eventArgType}>"; - - a.PropertyName = attribute; - - a.IsDirectiveAttribute = true; - - // Make this weakly typed (don't type check) - delegates have their own type-checking - // logic that we don't want to interfere with. - a.IsWeaklyTyped = true; - - if (enablePreventDefault) - { - a.BindAttributeParameter(parameter => - { - parameter.Name = "preventDefault"; - parameter.PropertyName = "PreventDefault"; - parameter.TypeName = typeof(bool).FullName; - parameter.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.EventHandlerTagHelper_PreventDefault, - attributeName)); - }); - } - - if (enableStopPropagation) - { - a.BindAttributeParameter(parameter => - { - parameter.Name = "stopPropagation"; - parameter.PropertyName = "StopPropagation"; - parameter.TypeName = typeof(bool).FullName; - parameter.SetDocumentation( - DocumentationDescriptor.From( - DocumentationId.EventHandlerTagHelper_StopPropagation, - attributeName)); - }); - } - }); - - return builder.Build(); - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ITagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ITagHelperDescriptorProvider.cs deleted file mode 100644 index 1ce96e5a161..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ITagHelperDescriptorProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Razor.Language; - -public interface ITagHelperDescriptorProvider : IRazorEngineFeature -{ - int Order { get; } - - void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default); -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ITagHelperDiscoveryService.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ITagHelperDiscoveryService.cs new file mode 100644 index 00000000000..1c6497487fa --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ITagHelperDiscoveryService.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language; + +internal interface ITagHelperDiscoveryService : IRazorEngineFeature +{ + TagHelperCollection GetTagHelpers(Compilation compilation, TagHelperDiscoveryOptions options, CancellationToken cancellationToken = default); + TagHelperCollection GetTagHelpers(Compilation compilation, CancellationToken cancellationToken = default); + + bool TryGetDiscoverer(Compilation compilation, TagHelperDiscoveryOptions options, [NotNullWhen(true)] out TagHelperDiscoverer? discoverer); + bool TryGetDiscoverer(Compilation compilation, [NotNullWhen(true)] out TagHelperDiscoverer? discoverer); +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs index e755e4e8317..0863df71d18 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs @@ -356,6 +356,7 @@ private static void AddDefaultPhases(ImmutableArray.Builder p private static void AddDefaultFeatures(ImmutableArray.Builder features) { features.Add(new DefaultImportProjectFeature()); + features.Add(new TagHelperDiscoveryService()); // General extensibility features.Add(new ConfigureDirectivesFeature()); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngineBuilderExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngineBuilderExtensions.cs index b223f9efa50..d0b89c9a5a4 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngineBuilderExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngineBuilderExtensions.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Microsoft.CodeAnalysis.CSharp; using RazorExtensionsV1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.RazorExtensions; using RazorExtensionsV2_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.RazorExtensions; @@ -49,6 +50,18 @@ public static void RegisterExtensions(this RazorProjectEngineBuilder builder) } } + public static RazorProjectEngineBuilder RegisterDefaultTagHelperProducer(this RazorProjectEngineBuilder builder) + { + ArgHelper.ThrowIfNull(builder); + + if (!builder.Features.OfType().Any()) + { + builder.Features.Add(new DefaultTagHelperProducer.Factory()); + } + + return builder; + } + public static RazorProjectEngineBuilder ConfigureParserOptions(this RazorProjectEngineBuilder builder, Action configure) { ArgHelper.ThrowIfNull(builder); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SymbolCache.AssemblySymbolData.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SymbolCache.AssemblySymbolData.cs index ab381e77ff4..c3f249a8daf 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SymbolCache.AssemblySymbolData.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SymbolCache.AssemblySymbolData.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Razor; using Microsoft.CodeAnalysis; @@ -12,6 +15,14 @@ internal partial class SymbolCache { public sealed partial class AssemblySymbolData(IAssemblySymbol symbol) { + private readonly ConcurrentDictionary _tagHelpers = []; + + public bool TryGetTagHelpers(int key, [NotNullWhen(true)] out TagHelperCollection? value) + => _tagHelpers.TryGetValue(key, out value); + + public TagHelperCollection AddTagHelpers(int key, TagHelperCollection value) + => _tagHelpers.GetOrAdd(key, value); + public bool MightContainTagHelpers { get; } = CalculateMightContainTagHelpers(symbol); private static bool CalculateMightContainTagHelpers(IAssemblySymbol assembly) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperCollector.Cache.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperCollector.Cache.cs deleted file mode 100644 index 7bc9e56657b..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperCollector.Cache.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Threading; - -namespace Microsoft.AspNetCore.Razor.Language; - -public abstract partial class TagHelperCollector - where T : TagHelperCollector -{ - private sealed class Cache - { - private const int IncludeDocumentation = 1 << 0; - private const int ExcludeHidden = 1 << 1; - - // The cache needs to be large enough to handle all combinations of options. - private const int CacheSize = (IncludeDocumentation | ExcludeHidden) + 1; - - private readonly TagHelperDescriptor[]?[] _tagHelpers = new TagHelperDescriptor[CacheSize][]; - - public bool TryGet(bool includeDocumentation, bool excludeHidden, [NotNullWhen(true)] out TagHelperDescriptor[]? tagHelpers) - { - var index = CalculateIndex(includeDocumentation, excludeHidden); - - tagHelpers = Volatile.Read(ref _tagHelpers[index]); - return tagHelpers is not null; - } - - public TagHelperDescriptor[] Add(TagHelperDescriptor[] tagHelpers, bool includeDocumentation, bool excludeHidden) - { - var index = CalculateIndex(includeDocumentation, excludeHidden); - - return InterlockedOperations.Initialize(ref _tagHelpers[index], tagHelpers); - } - - private static int CalculateIndex(bool includeDocumentation, bool excludeHidden) - { - var index = 0; - - if (includeDocumentation) - { - index |= IncludeDocumentation; - } - - if (excludeHidden) - { - index |= ExcludeHidden; - } - - return index; - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperCollector.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperCollector.cs deleted file mode 100644 index 1d9baaadab7..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperCollector.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Razor.Language; - -public abstract partial class TagHelperCollector( - Compilation compilation, - IAssemblySymbol? targetAssembly) - where T : TagHelperCollector -{ - // This type is generic to ensure that each descendent gets its own instance of this field. - private static readonly ConditionalWeakTable s_perAssemblyCaches = new(); - - private readonly Compilation _compilation = compilation; - private readonly IAssemblySymbol? _targetAssembly = targetAssembly; - - protected virtual bool IncludeNestedTypes => false; - - protected abstract bool IsCandidateType(INamedTypeSymbol type); - - protected abstract void Collect( - INamedTypeSymbol type, - ICollection results, - CancellationToken cancellationToken); - - public void Collect(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken) - { - if (_targetAssembly is not null) - { - Collect(_targetAssembly, context.Results, cancellationToken); - } - else - { - Collect(_compilation.Assembly, context.Results, cancellationToken); - - foreach (var reference in _compilation.References) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) - { - // Check to see if we already have tag helpers cached for this assembly - // and use the cached versions if we do. Roslyn shares PE assembly symbols - // across compilations, so this ensures that we don't produce new tag helpers - // for the same assemblies over and over again. - - var assemblySymbolData = SymbolCache.GetAssemblySymbolData(assembly); - if (!assemblySymbolData.MightContainTagHelpers) - { - continue; - } - - var includeDocumentation = context.IncludeDocumentation; - var excludeHidden = context.ExcludeHidden; - - var cache = s_perAssemblyCaches.GetValue(assembly, static assembly => new Cache()); - if (!cache.TryGet(includeDocumentation, excludeHidden, out var tagHelpers)) - { - using var _ = ListPool.GetPooledObject(out var referenceTagHelpers); - Collect(assembly, referenceTagHelpers, cancellationToken); - - tagHelpers = cache.Add(referenceTagHelpers.ToArrayOrEmpty(), includeDocumentation, excludeHidden); - } - - foreach (var tagHelper in tagHelpers) - { - context.Results.Add(tagHelper); - } - } - } - } - } - - protected virtual void Collect( - IAssemblySymbol assembly, - ICollection results, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var includeNestedTypes = IncludeNestedTypes; - - using var stack = new PooledArrayBuilder(); - - stack.Push(assembly.GlobalNamespace); - - while (stack.Count > 0) - { - cancellationToken.ThrowIfCancellationRequested(); - - var namespaceOrType = stack.Pop(); - - switch (namespaceOrType.Kind) - { - case SymbolKind.Namespace: - var members = namespaceOrType.GetMembers(); - - // Note: Push members onto the stack in reverse to ensure - // that they're popped off and processed in the correct order. - for (var i = members.Length - 1; i >= 0; i--) - { - cancellationToken.ThrowIfCancellationRequested(); - - // Namespaces members are only ever namespaces or types. - stack.Push((INamespaceOrTypeSymbol)members[i]); - } - - break; - - case SymbolKind.NamedType: - var typeSymbol = (INamedTypeSymbol)namespaceOrType; - - if (IsCandidateType(typeSymbol)) - { - // We have a candidate. Collect it. - Collect(typeSymbol, results, cancellationToken); - } - - if (includeNestedTypes && namespaceOrType.DeclaredAccessibility == Accessibility.Public) - { - var typeMembers = namespaceOrType.GetTypeMembers(); - - // Note: Push members onto the stack in reverse to ensure - // that they're popped off and processed in the correct order. - for (var i = typeMembers.Length - 1; i >= 0; i--) - { - cancellationToken.ThrowIfCancellationRequested(); - - stack.Push(typeMembers[i]); - } - } - - break; - } - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDescriptorProviderBase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDescriptorProviderBase.cs deleted file mode 100644 index 9f0fe345aba..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDescriptorProviderBase.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Razor.Language; - -public abstract class TagHelperDescriptorProviderBase(int order = 0) : RazorEngineFeatureBase, ITagHelperDescriptorProvider -{ - public int Order { get; } = order; - - public abstract void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default); -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDescriptorProviderContext.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDescriptorProviderContext.cs deleted file mode 100644 index 7a755aaab8e..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDescriptorProviderContext.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Razor.Language; - -public sealed class TagHelperDescriptorProviderContext( - Compilation compilation, - IAssemblySymbol? targetAssembly, - ICollection results) -{ - public Compilation Compilation { get; } = compilation; - public IAssemblySymbol? TargetAssembly { get; } = targetAssembly; - public ICollection Results { get; } = results; - - public bool ExcludeHidden { get; init; } - public bool IncludeDocumentation { get; init; } - - public TagHelperDescriptorProviderContext(Compilation compilation, IAssemblySymbol? targetAssembly = null) - : this(compilation, targetAssembly, results: []) - { - } - - public TagHelperDescriptorProviderContext(Compilation compilation, ICollection results) - : this(compilation, targetAssembly: null, results) - { - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoverer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoverer.cs new file mode 100644 index 00000000000..9fc6f5ec5fd --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoverer.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language; + +internal sealed class TagHelperDiscoverer(ImmutableArray producers, bool includeDocumentation, bool excludeHidden) +{ + private readonly int _cacheKey = GetCacheKey(producers, includeDocumentation, excludeHidden); + + /// + /// Generates a unique integer cache key based on the specified set of TagHelper producers and option flags. + /// + /// + /// The generated cache key is intended for efficient lookup scenarios where the combination of + /// producers and options must be uniquely identified. The method supports up to 30 distinct TagHelperProducer + /// kinds. + /// + private static int GetCacheKey(ImmutableArray producers, bool includeDocumentation, bool excludeHidden) + { + Debug.Assert(producers.Length <= 30, "Too many TagHelperProducer kinds to fit in a cache key."); + + var key = 0; + + if (includeDocumentation) + { + key |= 1 << 0; + } + + if (excludeHidden) + { + key |= 1 << 1; + } + + foreach (var producer in producers) + { + key |= 1 << ((int)producer.Kind + 2); + } + + return key; + } + + public TagHelperCollection GetTagHelpers(IAssemblySymbol assembly, CancellationToken cancellationToken = default) + { + if (producers.IsDefaultOrEmpty) + { + return TagHelperCollection.Empty; + } + + // Optimization: Check to see if this assembly might contain tag helpers before doing any work. + var assemblySymbolData = SymbolCache.GetAssemblySymbolData(assembly); + if (!assemblySymbolData.MightContainTagHelpers) + { + return TagHelperCollection.Empty; + } + + // Check to see if we already have tag helpers cached for this assembly + // and use the cached versions if we do. Roslyn shares PE assembly symbols + // across compilations, so this ensures that we don't produce new tag helpers + // for the same assemblies over and over again. + + if (assemblySymbolData.TryGetTagHelpers(_cacheKey, out var tagHelpers)) + { + return tagHelpers; + } + + // We don't have tag helpers cached for this assembly, so we have to discover them. + var builder = new TagHelperCollection.RefBuilder(); + try + { + // First, let producers add any static tag helpers they might have. + // Also, capture any producers that need to analyze types. + using var _ = ArrayPool.Shared.GetPooledArraySpan( + minimumLength: producers.Length, clearOnReturn: true, out var typeProducers); + + var index = 0; + var includeNestedTypes = false; + + foreach (var producer in producers) + { + if (producer.SupportsStaticTagHelpers) + { + producer.AddStaticTagHelpers(assembly, ref builder); + } + + if (producer.SupportsTypes) + { + typeProducers[index++] = producer; + includeNestedTypes |= producer.SupportsNestedTypes; + } + } + + typeProducers = typeProducers[..index]; + + cancellationToken.ThrowIfCancellationRequested(); + + // Did another discovery request for the same assembly finish and + // cache the result while we were producing static tag helpers? + if (assemblySymbolData.TryGetTagHelpers(_cacheKey, out tagHelpers)) + { + return tagHelpers; + } + + // Now, walk all types in the assembly and let producers add tag helpers. + using var stack = new MemoryBuilder(initialCapacity: 32, clearArray: true); + + stack.Push(assembly.GlobalNamespace); + + while (!stack.IsEmpty) + { + cancellationToken.ThrowIfCancellationRequested(); + + var namespaceOrType = stack.Pop(); + + switch (namespaceOrType.Kind) + { + case SymbolKind.Namespace: + var members = namespaceOrType.GetMembers(); + + // Note: Push members onto the stack in reverse to ensure + // that they're popped off and processed in the correct order. + for (var i = members.Length - 1; i >= 0; i--) + { + // Namespaces members are only ever namespaces or types. + stack.Push((INamespaceOrTypeSymbol)members[i]); + } + + break; + + case SymbolKind.NamedType: + var typeSymbol = (INamedTypeSymbol)namespaceOrType; + + foreach (var producer in typeProducers) + { + if (producer.IsCandidateType(typeSymbol)) + { + producer.AddTagHelpersForType(typeSymbol, ref builder, cancellationToken); + } + } + + if (includeNestedTypes && namespaceOrType.DeclaredAccessibility == Accessibility.Public) + { + var typeMembers = namespaceOrType.GetTypeMembers(); + + // Note: Push members onto the stack in reverse to ensure + // that they're popped off and processed in the correct order. + for (var i = typeMembers.Length - 1; i >= 0; i--) + { + stack.Push(typeMembers[i]); + } + } + + break; + } + } + + + return assemblySymbolData.AddTagHelpers(_cacheKey, builder.ToCollection()); + } + finally + { + builder.Dispose(); + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoveryOptions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoveryOptions.cs new file mode 100644 index 00000000000..666bb6bc69b --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoveryOptions.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Razor.Language; + +internal enum TagHelperDiscoveryOptions : byte +{ + ExcludeHidden = 1 << 0, + IncludeDocumentation = 1 << 1 +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoveryService.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoveryService.cs new file mode 100644 index 00000000000..f3a351f5dcc --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoveryService.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language; + +internal sealed class TagHelperDiscoveryService : RazorEngineFeatureBase, ITagHelperDiscoveryService +{ + private ImmutableArray _producerFactories; + + protected override void OnInitialized() + { + _producerFactories = Engine.GetFeatures(); + } + + public TagHelperCollection GetTagHelpers( + Compilation compilation, + TagHelperDiscoveryOptions options, + CancellationToken cancellationToken = default) + => GetTagHelpersForCompilation(compilation, options, cancellationToken); + + public TagHelperCollection GetTagHelpers( + Compilation compilation, + CancellationToken cancellationToken = default) + => GetTagHelpersForCompilation(compilation, options: default, cancellationToken); + + private TagHelperCollection GetTagHelpersForCompilation( + Compilation compilation, + TagHelperDiscoveryOptions options, + CancellationToken cancellationToken = default) + { + ArgHelper.ThrowIfNull(compilation); + + if (!TryGetDiscoverer(compilation, options, out var discoverer)) + { + return TagHelperCollection.Empty; + } + + using var collections = new MemoryBuilder(initialCapacity: 512, clearArray: true); + + if (compilation.Assembly is { } compilationAssembly) + { + var collection = discoverer.GetTagHelpers(compilationAssembly, cancellationToken); + if (!collection.IsEmpty) + { + collections.Append(collection); + } + } + + foreach (var reference in compilation.References) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol referenceAssembly) + { + var collection = discoverer.GetTagHelpers(referenceAssembly, cancellationToken); + if (!collection.IsEmpty) + { + collections.Append(collection); + } + } + } + + return TagHelperCollection.Merge(collections.AsMemory().Span); + } + + public bool TryGetDiscoverer(Compilation compilation, TagHelperDiscoveryOptions options, [NotNullWhen(true)] out TagHelperDiscoverer? discoverer) + { + ArgHelper.ThrowIfNull(compilation); + + var excludeHidden = options.IsFlagSet(TagHelperDiscoveryOptions.ExcludeHidden); + var includeDocumentation = options.IsFlagSet(TagHelperDiscoveryOptions.IncludeDocumentation); + + var producers = GetProducers(compilation, includeDocumentation, excludeHidden); + + if (producers.IsEmpty) + { + discoverer = default; + return false; + } + + discoverer = new TagHelperDiscoverer(producers, includeDocumentation, excludeHidden); + return true; + } + + public bool TryGetDiscoverer(Compilation compilation, [NotNullWhen(true)] out TagHelperDiscoverer? discoverer) + => TryGetDiscoverer(compilation, options: default, out discoverer); + + private ImmutableArray GetProducers(Compilation compilation, bool includeDocumentation, bool excludeHidden) + { + if (_producerFactories.IsDefaultOrEmpty) + { + return []; + } + + using var builder = new PooledArrayBuilder(_producerFactories.Length); + + foreach (var factory in _producerFactories) + { + if (factory.TryCreate(compilation, includeDocumentation, excludeHidden, out var producer)) + { + builder.Add(producer); + } + } + + return builder.ToImmutableAndClear(); + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/BindTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/BindTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..f519a5da1ea --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/BindTagHelperProducer.Factory.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class BindTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ComponentsApi.BindConverter.FullTypeName, out var bindConverterType)) + { + result = null; + return false; + } + + var bindElementAttributeType = compilation.GetTypeByMetadataName(ComponentsApi.BindElementAttribute.FullTypeName); + var bindInputElementAttributeType = compilation.GetTypeByMetadataName(ComponentsApi.BindInputElementAttribute.FullTypeName); + + result = new BindTagHelperProducer(bindConverterType, bindElementAttributeType, bindInputElementAttributeType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/BindTagHelperProducer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/BindTagHelperProducer.cs new file mode 100644 index 00000000000..af79ad9668a --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/BindTagHelperProducer.cs @@ -0,0 +1,650 @@ +// 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 System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class BindTagHelperProducer : TagHelperProducer +{ + // This provider returns tag helper information for 'bind' which doesn't necessarily + // map to any real component. Bind behaves more like a macro, which can map a single LValue to + // both a 'value' attribute and a 'value changed' attribute. + // + // User types: + // + // + // We generate: + // + // + // This isn't very different from code the user could write themselves - thus the pronouncement + // that @bind is very much like a macro. + // + // A lot of the value that provide in this case is that the associations between the + // elements, and the attributes aren't straightforward. + // + // For instance on we need to listen to 'value' and 'onchange', + // but on + // and so we have a special case for input elements and their type attributes. + // + // Additionally, our mappings tell us about cases like where + // we need to treat the value as an invariant culture value. In general the HTML5 field + // types use invariant culture values when interacting with the DOM, in contrast to + // which is free-form text and is most likely to be + // culture-sensitive. + // + // 4. For components, we have a bit of a special case. We can infer a syntax that matches + // case #2 based on property names. So if a component provides both 'Value' and 'ValueChanged' + // we will turn that into an instance of bind. + // + // So case #1 here is the most general case. Case #2 and #3 are data-driven based on attribute data + // we have. Case #4 is data-driven based on component definitions. + // + // We provide a good set of attributes that map to the HTML dom. This set is user extensible. + + private static readonly Lazy s_fallbackTagHelper = new(CreateFallbackBindTagHelper); + + private readonly INamedTypeSymbol _bindConverterType; + private readonly INamedTypeSymbol? _bindElementAttributeType; + private readonly INamedTypeSymbol? _bindInputElementAttributeType; + + private BindTagHelperProducer( + INamedTypeSymbol bindConverterType, + INamedTypeSymbol? bindElementAttributeType, + INamedTypeSymbol? bindInputElementAttributeType) + { + _bindConverterType = bindConverterType; + _bindElementAttributeType = bindElementAttributeType; + _bindInputElementAttributeType = bindInputElementAttributeType; + } + + public override TagHelperProducerKind Kind => TagHelperProducerKind.Bind; + + public override bool SupportsStaticTagHelpers => true; + + public override void AddStaticTagHelpers(IAssemblySymbol assembly, ref TagHelperCollection.RefBuilder results) + { + if (!SymbolEqualityComparer.Default.Equals(assembly, _bindConverterType.ContainingAssembly)) + { + return; + } + + // Tag Helper definition for case #1. This is the most general case. + results.Add(s_fallbackTagHelper.Value); + } + + public override bool SupportsTypes + => _bindElementAttributeType is not null && _bindInputElementAttributeType is not null; + + public override bool IsCandidateType(INamedTypeSymbol type) + => type.DeclaredAccessibility == Accessibility.Public && + type.Name == "BindAttributes"; + + public override void AddTagHelpersForType( + INamedTypeSymbol type, + ref TagHelperCollection.RefBuilder results, + CancellationToken cancellationToken) + { + // Not handling duplicates here for now since we're the primary ones extending this. + // If we see users adding to the set of 'bind' constructs we will want to add deduplication + // and potentially diagnostics. + foreach (var attribute in type.GetAttributes()) + { + var constructorArguments = attribute.ConstructorArguments; + + TagHelperDescriptor? tagHelper = null; + + // For case #2 & #3 we have a whole bunch of attribute entries on BindMethods that we can use + // to data-drive the definitions of these tag helpers. + + // We need to check the constructor argument length here, because this can show up as 0 + // if the language service fails to initialize. This is an invalid case, so skip it. + if (constructorArguments.Length == 4 && SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _bindElementAttributeType)) + { + tagHelper = CreateElementBindTagHelper( + typeName: type.GetDefaultDisplayString(), + typeNamespace: type.ContainingNamespace.GetFullName(), + typeNameIdentifier: type.Name, + element: (string?)constructorArguments[0].Value, + typeAttribute: null, + suffix: (string?)constructorArguments[1].Value, + valueAttribute: (string?)constructorArguments[2].Value, + changeAttribute: (string?)constructorArguments[3].Value); + } + else if (constructorArguments.Length == 4 && SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _bindInputElementAttributeType)) + { + tagHelper = CreateElementBindTagHelper( + typeName: type.GetDefaultDisplayString(), + typeNamespace: type.ContainingNamespace.GetFullName(), + typeNameIdentifier: type.Name, + element: "input", + typeAttribute: (string?)constructorArguments[0].Value, + suffix: (string?)constructorArguments[1].Value, + valueAttribute: (string?)constructorArguments[2].Value, + changeAttribute: (string?)constructorArguments[3].Value); + } + else if (constructorArguments.Length == 6 && SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _bindInputElementAttributeType)) + { + tagHelper = CreateElementBindTagHelper( + typeName: type.GetDefaultDisplayString(), + typeNamespace: type.ContainingNamespace.GetFullName(), + typeNameIdentifier: type.Name, + element: "input", + typeAttribute: (string?)constructorArguments[0].Value, + suffix: (string?)constructorArguments[1].Value, + valueAttribute: (string?)constructorArguments[2].Value, + changeAttribute: (string?)constructorArguments[3].Value, + isInvariantCulture: (bool?)constructorArguments[4].Value ?? false, + format: (string?)constructorArguments[5].Value); + } + + if (tagHelper is not null) + { + results.Add(tagHelper); + } + } + } + + private static TagHelperDescriptor CreateElementBindTagHelper( + string typeName, + string typeNamespace, + string typeNameIdentifier, + string? element, + string? typeAttribute, + string? suffix, + string? valueAttribute, + string? changeAttribute, + bool isInvariantCulture = false, + string? format = null) + { + string name, attributeName, formatName, formatAttributeName, eventName; + + if (suffix is { } s) + { + name = "Bind_" + s; + attributeName = "@bind-" + s; + formatName = "Format_" + s; + formatAttributeName = "format-" + s; + eventName = "Event_" + s; + } + else + { + name = "Bind"; + attributeName = "@bind"; + + suffix = valueAttribute; + formatName = "Format_" + suffix; + formatAttributeName = "format-" + suffix; + eventName = "Event_" + suffix; + } + + using var _ = TagHelperDescriptorBuilder.GetPooledInstance( + TagHelperKind.Bind, name, ComponentsApi.AssemblyName, + out var builder); + + builder.SetTypeName(typeName, typeNamespace, typeNameIdentifier); + + builder.CaseSensitive = true; + builder.ClassifyAttributesOnly = true; + builder.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.BindTagHelper_Element, + valueAttribute, + changeAttribute)); + + var metadata = new BindMetadata.Builder + { + ValueAttribute = valueAttribute, + ChangeAttribute = changeAttribute, + IsInvariantCulture = isInvariantCulture, + Format = format + }; + + if (typeAttribute != null) + { + // For entries that map to the element, we need to be able to know + // the difference between and for which we + // want to use the same attributes. + // + // We provide a tag helper for that should match all input elements, + // but we only want it to be used when a more specific one is used. + // + // Therefore we use this metadata to know which one is more specific when two + // tag helpers match. + metadata.TypeAttribute = typeAttribute; + } + + builder.SetMetadata(metadata.Build()); + + builder.TagMatchingRule(rule => + { + rule.TagName = element; + if (typeAttribute != null) + { + rule.Attribute(a => + { + a.Name = "type"; + a.NameComparison = RequiredAttributeNameComparison.FullMatch; + a.Value = typeAttribute; + a.ValueComparison = RequiredAttributeValueComparison.FullMatch; + }); + } + + rule.Attribute(a => + { + a.Name = attributeName; + a.NameComparison = RequiredAttributeNameComparison.FullMatch; + a.IsDirectiveAttribute = true; + }); + }); + + builder.TagMatchingRule(rule => + { + rule.TagName = element; + if (typeAttribute != null) + { + rule.Attribute(a => + { + a.Name = "type"; + a.NameComparison = RequiredAttributeNameComparison.FullMatch; + a.Value = typeAttribute; + a.ValueComparison = RequiredAttributeValueComparison.FullMatch; + }); + } + + rule.Attribute(a => + { + a.Name = $"{attributeName}:get"; + a.NameComparison = RequiredAttributeNameComparison.FullMatch; + a.IsDirectiveAttribute = true; + }); + + rule.Attribute(a => + { + a.Name = $"{attributeName}:set"; + a.NameComparison = RequiredAttributeNameComparison.FullMatch; + a.IsDirectiveAttribute = true; + }); + }); + + builder.BindAttribute(a => + { + a.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.BindTagHelper_Element, + valueAttribute, + changeAttribute)); + + a.Name = attributeName; + a.TypeName = typeof(object).FullName; + a.IsDirectiveAttribute = true; + a.PropertyName = name; + + a.BindAttributeParameter(parameter => + { + parameter.Name = "format"; + parameter.PropertyName = formatName; + parameter.TypeName = typeof(string).FullName; + parameter.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.BindTagHelper_Element_Format, + attributeName)); + }); + + a.BindAttributeParameter(parameter => + { + parameter.Name = "event"; + parameter.PropertyName = eventName; + parameter.TypeName = typeof(string).FullName; + parameter.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.BindTagHelper_Element_Event, + attributeName)); + }); + + a.BindAttributeParameter(parameter => + { + parameter.Name = "culture"; + parameter.PropertyName = "Culture"; + parameter.TypeName = typeof(CultureInfo).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Culture); + }); + + a.BindAttributeParameter(parameter => + { + parameter.Name = "get"; + parameter.PropertyName = "Get"; + parameter.TypeName = typeof(object).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Get); + parameter.BindAttributeGetSet = true; + }); + + a.BindAttributeParameter(parameter => + { + parameter.Name = "set"; + parameter.PropertyName = "Set"; + parameter.TypeName = typeof(Delegate).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Set); + }); + + a.BindAttributeParameter(parameter => + { + parameter.Name = "after"; + parameter.PropertyName = "After"; + parameter.TypeName = typeof(Delegate).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_After); + }); + }); + + // This is no longer supported. This is just here so we can add a diagnostic later on when this matches. + builder.BindAttribute(attribute => + { + attribute.Name = formatAttributeName; + attribute.TypeName = "System.String"; + attribute.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.BindTagHelper_Element_Format, + attributeName)); + + attribute.PropertyName = formatName; + }); + + return builder.Build(); + } + + public void AddTagHelpersForComponent(TagHelperDescriptor tagHelper, ref TagHelperCollection.RefBuilder results) + { + if (tagHelper.Kind != TagHelperKind.Component || !SupportsTypes) + { + return; + } + + // We want to create a 'bind' tag helper everywhere we see a pair of properties like `Foo`, `FooChanged` + // where `FooChanged` is a delegate and `Foo` is not. + // + // The easiest way to figure this out without a lot of backtracking is to look for `FooChanged` and then + // try to find a matching "Foo". + // + // We also look for a corresponding FooExpression attribute, though its presence is optional. + foreach (var changeAttribute in tagHelper.BoundAttributes) + { + if (!changeAttribute.Name.EndsWith("Changed", StringComparison.Ordinal) || + + // Allow the ValueChanged attribute to be a delegate or EventCallback<>. + // + // We assume that the Delegate or EventCallback<> has a matching type, and the C# compiler will help + // you figure figure it out if you did it wrongly. + (!changeAttribute.IsDelegateProperty() && !changeAttribute.IsEventCallbackProperty())) + { + continue; + } + + BoundAttributeDescriptor? valueAttribute = null; + BoundAttributeDescriptor? expressionAttribute = null; + + var valueAttributeName = changeAttribute.Name[..^"Changed".Length]; + var expressionAttributeName = valueAttributeName + "Expression"; + + foreach (var attribute in tagHelper.BoundAttributes) + { + if (attribute.Name == valueAttributeName) + { + valueAttribute = attribute; + } + + if (attribute.Name == expressionAttributeName) + { + expressionAttribute = attribute; + } + + if (valueAttribute != null && expressionAttribute != null) + { + // We found both, so we can stop looking now + break; + } + } + + if (valueAttribute == null) + { + // No matching attribute found. + continue; + } + + using var _ = TagHelperDescriptorBuilder.GetPooledInstance( + TagHelperKind.Bind, tagHelper.Name, tagHelper.AssemblyName, + out var builder); + + builder.SetTypeName(tagHelper.TypeNameObject); + + builder.DisplayName = tagHelper.DisplayName; + builder.CaseSensitive = true; + builder.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.BindTagHelper_Component, + valueAttribute.Name, + changeAttribute.Name)); + + var metadata = new BindMetadata.Builder + { + ValueAttribute = valueAttribute.Name, + ChangeAttribute = changeAttribute.Name + }; + + if (expressionAttribute != null) + { + metadata.ExpressionAttribute = expressionAttribute.Name; + } + + // Match the component and attribute name + builder.TagMatchingRule(rule => + { + rule.TagName = tagHelper.TagMatchingRules.Single().TagName; + rule.Attribute(attribute => + { + attribute.Name = "@bind-" + valueAttribute.Name; + attribute.NameComparison = RequiredAttributeNameComparison.FullMatch; + attribute.IsDirectiveAttribute = true; + }); + }); + + builder.TagMatchingRule(rule => + { + rule.TagName = tagHelper.TagMatchingRules.Single().TagName; + rule.Attribute(attribute => + { + attribute.Name = "@bind-" + valueAttribute.Name + ":get"; + attribute.NameComparison = RequiredAttributeNameComparison.FullMatch; + attribute.IsDirectiveAttribute = true; + }); + rule.Attribute(attribute => + { + attribute.Name = "@bind-" + valueAttribute.Name + ":set"; + attribute.NameComparison = RequiredAttributeNameComparison.FullMatch; + attribute.IsDirectiveAttribute = true; + }); + }); + + builder.BindAttribute(attribute => + { + attribute.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.BindTagHelper_Component, + valueAttribute.Name, + changeAttribute.Name)); + + attribute.Name = "@bind-" + valueAttribute.Name; + attribute.TypeName = changeAttribute.TypeName; + attribute.IsEnum = valueAttribute.IsEnum; + attribute.ContainingType = valueAttribute.ContainingType; + attribute.IsDirectiveAttribute = true; + attribute.PropertyName = valueAttribute.PropertyName; + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "get"; + parameter.PropertyName = "Get"; + parameter.TypeName = typeof(object).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Get); + parameter.BindAttributeGetSet = true; + }); + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "set"; + parameter.PropertyName = "Set"; + parameter.TypeName = typeof(Delegate).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Set); + }); + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "after"; + parameter.PropertyName = "After"; + parameter.TypeName = typeof(Delegate).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_After); + }); + }); + + if (tagHelper.IsFullyQualifiedNameMatch) + { + builder.IsFullyQualifiedNameMatch = true; + } + + builder.SetMetadata(metadata.Build()); + + results.Add(builder.Build()); + } + } + + private static TagHelperDescriptor CreateFallbackBindTagHelper() + { + using var _ = TagHelperDescriptorBuilder.GetPooledInstance( + TagHelperKind.Bind, "Bind", ComponentsApi.AssemblyName, + out var builder); + + builder.SetTypeName( + fullName: "Microsoft.AspNetCore.Components.Bind", + typeNamespace: "Microsoft.AspNetCore.Components", + typeNameIdentifier: "Bind"); + + builder.CaseSensitive = true; + builder.ClassifyAttributesOnly = true; + builder.SetDocumentation(DocumentationDescriptor.BindTagHelper_Fallback); + + builder.SetMetadata(new BindMetadata() { IsFallback = true }); + + builder.TagMatchingRule(rule => + { + rule.TagName = "*"; + rule.Attribute(attribute => + { + attribute.Name = "@bind-"; + attribute.NameComparison = RequiredAttributeNameComparison.PrefixMatch; + attribute.IsDirectiveAttribute = true; + }); + }); + + builder.BindAttribute(attribute => + { + attribute.SetDocumentation(DocumentationDescriptor.BindTagHelper_Fallback); + + var attributeName = "@bind-..."; + attribute.Name = attributeName; + attribute.AsDictionary("@bind-", typeof(object).FullName); + attribute.IsDirectiveAttribute = true; + + attribute.PropertyName = "Bind"; + + attribute.TypeName = "System.Collections.Generic.Dictionary"; + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "format"; + parameter.PropertyName = "Format"; + parameter.TypeName = typeof(string).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Fallback_Format); + }); + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "event"; + parameter.PropertyName = "Event"; + parameter.TypeName = typeof(string).FullName; + parameter.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.BindTagHelper_Fallback_Event, attributeName)); + }); + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "culture"; + parameter.PropertyName = "Culture"; + parameter.TypeName = typeof(CultureInfo).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Culture); + }); + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "get"; + parameter.PropertyName = "Get"; + parameter.TypeName = typeof(object).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Get); + parameter.BindAttributeGetSet = true; + }); + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "set"; + parameter.PropertyName = "Set"; + parameter.TypeName = typeof(Delegate).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_Set); + }); + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "after"; + parameter.PropertyName = "After"; + parameter.TypeName = typeof(Delegate).FullName; + parameter.SetDocumentation(DocumentationDescriptor.BindTagHelper_Element_After); + }); + }); + + return builder.Build(); + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ComponentTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ComponentTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..98d5cace992 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ComponentTagHelperProducer.Factory.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class ComponentTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + private BindTagHelperProducer.Factory? _bindTagHelperProducerFactory; + + protected override void OnInitialized() + { + _bindTagHelperProducerFactory = GetRequiredFeature(); + } + + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + Assumed.NotNull(_bindTagHelperProducerFactory); + + _bindTagHelperProducerFactory.TryCreate(compilation, includeDocumentation, excludeHidden, out var producer); + + result = new ComponentTagHelperProducer((BindTagHelperProducer?)producer); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ComponentTagHelperProducer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ComponentTagHelperProducer.cs new file mode 100644 index 00000000000..d454848755b --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ComponentTagHelperProducer.cs @@ -0,0 +1,764 @@ +// 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 System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class ComponentTagHelperProducer : TagHelperProducer +{ + private readonly BindTagHelperProducer? _bindTagHelperProducer; + + private ComponentTagHelperProducer(BindTagHelperProducer? bindTagHelperProducer) + { + _bindTagHelperProducer = bindTagHelperProducer; + } + + public override TagHelperProducerKind Kind => TagHelperProducerKind.Component; + + public override bool SupportsTypes => true; + + public override bool IsCandidateType(INamedTypeSymbol type) + => ComponentDetectionConventions.IsComponent(type, ComponentsApi.IComponent.MetadataName); + + public override void AddTagHelpersForType( + INamedTypeSymbol type, + ref TagHelperCollection.RefBuilder results, + CancellationToken cancellationToken) + { + // Components have very simple matching rules. + // 1. The type name (short) matches the tag name. + // 2. The fully qualified name matches the tag name. + + // First, compute the relevant properties for this type so that we + // don't need to compute them twice. + var properties = GetProperties(type); + + var shortNameMatchingDescriptor = CreateShortNameMatchingDescriptor(type, properties); + results.Add(shortNameMatchingDescriptor); + + // If the component is in the global namespace, skip adding this descriptor which will be the same as the short name one. + TagHelperDescriptor? fullyQualifiedNameMatchingDescriptor = null; + if (!type.ContainingNamespace.IsGlobalNamespace) + { + fullyQualifiedNameMatchingDescriptor = CreateFullyQualifiedNameMatchingDescriptor(type, properties); + results.Add(fullyQualifiedNameMatchingDescriptor); + } + + // Produce bind tag helpers for the component. + if (_bindTagHelperProducer is { SupportsTypes: true }) + { + _bindTagHelperProducer.AddTagHelpersForComponent(shortNameMatchingDescriptor, ref results); + + if (fullyQualifiedNameMatchingDescriptor is not null) + { + _bindTagHelperProducer.AddTagHelpersForComponent(fullyQualifiedNameMatchingDescriptor, ref results); + } + } + + foreach (var childContent in shortNameMatchingDescriptor.GetChildContentProperties()) + { + // Synthesize a separate tag helper for each child content property that's declared. + results.Add(CreateChildContentDescriptor(shortNameMatchingDescriptor, childContent)); + if (fullyQualifiedNameMatchingDescriptor is not null) + { + results.Add(CreateChildContentDescriptor(fullyQualifiedNameMatchingDescriptor, childContent)); + } + } + } + + private static TagHelperDescriptor CreateShortNameMatchingDescriptor( + INamedTypeSymbol type, + ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties) + => CreateNameMatchingDescriptor(type, properties, fullyQualified: false); + + private static TagHelperDescriptor CreateFullyQualifiedNameMatchingDescriptor( + INamedTypeSymbol type, + ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties) + => CreateNameMatchingDescriptor(type, properties, fullyQualified: true); + + private static TagHelperDescriptor CreateNameMatchingDescriptor( + INamedTypeSymbol type, + ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties, + bool fullyQualified) + { + var typeName = TypeNameObject.From(type); + var assemblyName = type.ContainingAssembly.Identity.Name; + + using var _ = TagHelperDescriptorBuilder.GetPooledInstance( + TagHelperKind.Component, typeName.FullName.AssumeNotNull(), assemblyName, out var builder); + + builder.RuntimeKind = RuntimeKind.IComponent; + builder.SetTypeName(typeName); + + var metadata = new ComponentMetadata.Builder(); + + builder.CaseSensitive = true; + + if (fullyQualified) + { + var fullName = type.ContainingNamespace.IsGlobalNamespace + ? type.Name + : $"{type.ContainingNamespace.GetFullName()}.{type.Name}"; + + builder.TagMatchingRule(r => + { + r.TagName = fullName; + }); + + builder.IsFullyQualifiedNameMatch = true; + } + else + { + builder.TagMatchingRule(r => + { + r.TagName = type.Name; + }); + } + + if (type.IsGenericType) + { + metadata.IsGeneric = true; + + using var cascadeGenericTypeAttributes = new PooledHashSet(StringComparer.Ordinal); + + foreach (var attribute in type.GetAttributes()) + { + if (attribute.HasFullName(ComponentsApi.CascadingTypeParameterAttribute.MetadataName) && + attribute.ConstructorArguments.FirstOrDefault() is { Value: string value }) + { + cascadeGenericTypeAttributes.Add(value); + } + } + + foreach (var typeArgument in type.TypeArguments) + { + if (typeArgument is ITypeParameterSymbol typeParameter) + { + var cascade = cascadeGenericTypeAttributes.Contains(typeParameter.Name); + CreateTypeParameterProperty(builder, typeParameter, cascade); + } + } + } + + if (HasRenderModeDirective(type)) + { + metadata.HasRenderModeDirective = true; + } + + var xml = type.GetDocumentationCommentXml(); + if (!string.IsNullOrEmpty(xml)) + { + builder.SetDocumentation(xml); + } + + foreach (var (property, kind) in properties) + { + if (kind == PropertyKind.Ignored) + { + continue; + } + + CreateProperty(builder, type, property, kind); + } + + if (builder.BoundAttributes.Any(static a => a.IsParameterizedChildContentProperty()) && + !builder.BoundAttributes.Any(static a => string.Equals(a.Name, ComponentHelpers.ChildContent.ParameterAttributeName, StringComparison.OrdinalIgnoreCase))) + { + // If we have any parameterized child content parameters, synthesize a 'Context' parameter to be + // able to set the variable name (for all child content). If the developer defined a 'Context' parameter + // already, then theirs wins. + CreateContextParameter(builder, childContentName: null); + } + + builder.SetMetadata(metadata.Build()); + + return builder.Build(); + } + + private static void CreateProperty(TagHelperDescriptorBuilder builder, INamedTypeSymbol containingSymbol, IPropertySymbol property, PropertyKind kind) + { + builder.BindAttribute(pb => + { + var builder = new PropertyMetadata.Builder(); + + pb.Name = property.Name; + pb.ContainingType = containingSymbol.GetFullName(); + pb.TypeName = property.Type.GetFullName(); + pb.PropertyName = property.Name; + pb.IsEditorRequired = property.GetAttributes().Any( + static a => a.HasFullName("Microsoft.AspNetCore.Components.EditorRequiredAttribute")); + + pb.CaseSensitive = false; + + builder.GloballyQualifiedTypeName = property.Type.GetGloballyQualifiedFullName(); + + if (kind == PropertyKind.Enum) + { + pb.IsEnum = true; + } + else if (kind == PropertyKind.ChildContent) + { + builder.IsChildContent = true; + } + else if (kind == PropertyKind.EventCallback) + { + builder.IsEventCallback = true; + } + else if (kind == PropertyKind.Delegate) + { + builder.IsDelegateSignature = true; + builder.IsDelegateWithAwaitableResult = IsAwaitable(property); + } + + if (HasTypeParameter(property.Type)) + { + builder.IsGenericTyped = true; + } + + if (property.SetMethod.AssumeNotNull().IsInitOnly) + { + builder.IsInitOnlyProperty = true; + } + + pb.SetMetadata(builder.Build()); + + var xml = property.GetDocumentationCommentXml(); + if (!string.IsNullOrEmpty(xml)) + { + pb.SetDocumentation(xml); + } + }); + + static bool HasTypeParameter(ITypeSymbol type) + { + if (type is ITypeParameterSymbol) + { + return true; + } + + // We need to check for cases like: + // [Parameter] public List MyProperty { get; set; } + // AND + // [Parameter] public List MyProperty { get; set; } + // + // We need to inspect the type arguments to tell the difference between a property that + // uses the containing class' type parameter(s) and a vanilla usage of generic types like + // List<> and Dictionary<,> + // + // Since we need to handle cases like RenderFragment>, this check must be recursive. + if (type is INamedTypeSymbol namedType && namedType.IsGenericType) + { + foreach (var typeArgument in namedType.TypeArguments) + { + if (HasTypeParameter(typeArgument)) + { + return true; + } + } + + // Another case to handle - if the type being inspected is a nested type + // inside a generic containing class. The common usage for this would be a case + // where a generic templated component defines a 'context' nested class. + if (namedType.ContainingType != null && HasTypeParameter(namedType.ContainingType)) + { + return true; + } + } + // Also check for cases like: + // [Parameter] public T[] MyProperty { get; set; } + else if (type is IArrayTypeSymbol array && HasTypeParameter(array.ElementType)) + { + return true; + } + + return false; + } + } + + private static bool IsAwaitable(IPropertySymbol prop) + { + var methodSymbol = ((INamedTypeSymbol)prop.Type).DelegateInvokeMethod.AssumeNotNull(); + if (methodSymbol.ReturnsVoid) + { + return false; + } + + var members = methodSymbol.ReturnType.GetMembers(); + foreach (var candidate in members) + { + if (candidate is not IMethodSymbol method || !string.Equals(candidate.Name, "GetAwaiter", StringComparison.Ordinal)) + { + continue; + } + + if (!VerifyGetAwaiter(method)) + { + continue; + } + + return true; + } + + return methodSymbol.IsAsync; + + static bool VerifyGetAwaiter(IMethodSymbol getAwaiter) + { + var returnType = getAwaiter.ReturnType; + if (returnType == null) + { + return false; + } + + var foundIsCompleted = false; + var foundOnCompleted = false; + var foundGetResult = false; + + foreach (var member in returnType.GetMembers()) + { + if (!foundIsCompleted && + member is IPropertySymbol property && + IsProperty_IsCompleted(property)) + { + foundIsCompleted = true; + } + + if (!(foundOnCompleted && foundGetResult) && member is IMethodSymbol method) + { + if (IsMethod_OnCompleted(method)) + { + foundOnCompleted = true; + } + else if (IsMethod_GetResult(method)) + { + foundGetResult = true; + } + } + + if (foundIsCompleted && foundOnCompleted && foundGetResult) + { + return true; + } + } + + return false; + + static bool IsProperty_IsCompleted(IPropertySymbol property) + { + return property is + { + Name: WellKnownMemberNames.IsCompleted, + Type.SpecialType: SpecialType.System_Boolean, + GetMethod: not null + }; + } + + static bool IsMethod_OnCompleted(IMethodSymbol method) + { + return method is + { + Name: WellKnownMemberNames.OnCompleted, + ReturnsVoid: true, + Parameters: [{ Type.TypeKind: TypeKind.Delegate }] + }; + } + + static bool IsMethod_GetResult(IMethodSymbol method) + { + return method is + { + Name: WellKnownMemberNames.GetResult, + Parameters: [] + }; + } + } + } + + private static void CreateTypeParameterProperty(TagHelperDescriptorBuilder builder, ITypeParameterSymbol typeParameter, bool cascade) + { + builder.BindAttribute(pb => + { + pb.DisplayName = typeParameter.Name; + pb.Name = typeParameter.Name; + pb.TypeName = typeof(Type).FullName; + pb.PropertyName = typeParameter.Name; + + var metadata = new TypeParameterMetadata.Builder + { + IsCascading = cascade + }; + + // Type constraints (like "Image" or "Foo") are stored independently of + // things like constructor constraints and not null constraints in the + // type parameter so we create a single string representation of all the constraints + // here. + using var constraints = new PooledList(); + + // CS0449: The 'class', 'struct', 'unmanaged', 'notnull', and 'default' constraints + // cannot be combined or duplicated, and must be specified first in the constraints list. + if (typeParameter.HasReferenceTypeConstraint) + { + constraints.Add("class"); + } + + if (typeParameter.HasNotNullConstraint) + { + constraints.Add("notnull"); + } + + if (typeParameter.HasUnmanagedTypeConstraint) + { + constraints.Add("unmanaged"); + } + else if (typeParameter.HasValueTypeConstraint) + { + // `HasValueTypeConstraint` is also true when `unmanaged` constraint is present. + constraints.Add("struct"); + } + + foreach (var constraintType in typeParameter.ConstraintTypes) + { + constraints.Add(constraintType.GetGloballyQualifiedFullName()); + } + + // CS0401: The new() constraint must be the last constraint specified. + if (typeParameter.HasConstructorConstraint) + { + constraints.Add("new()"); + } + + if (TryGetWhereClauseText(typeParameter, constraints, out var whereClauseText)) + { + metadata.Constraints = whereClauseText; + } + + // Collect attributes that should be propagated to the type inference method. + using var _ = StringBuilderPool.GetPooledObject(out var withAttributes); + foreach (var attribute in typeParameter.GetAttributes()) + { + if (attribute.HasFullName("System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute")) + { + Debug.Assert(attribute.AttributeClass != null); + + if (withAttributes.Length > 0) + { + withAttributes.Append(", "); + } + else + { + withAttributes.Append('['); + } + + withAttributes.Append(attribute.AttributeClass.GetGloballyQualifiedFullName()); + withAttributes.Append('('); + + var first = true; + foreach (var arg in attribute.ConstructorArguments) + { + if (first) + { + first = false; + } + else + { + withAttributes.Append(", "); + } + + if (arg.Kind == TypedConstantKind.Enum) + { + withAttributes.Append("unchecked(("); + withAttributes.Append(arg.Type!.GetGloballyQualifiedFullName()); + withAttributes.Append(')'); + withAttributes.Append(SymbolDisplay.FormatPrimitive(arg.Value!, quoteStrings: true, useHexadecimalNumbers: true)); + withAttributes.Append(')'); + } + else + { + Debug.Assert(false, $"Need to add support for '{arg.Kind}' and make sure the output is 'global::' prefixed."); + withAttributes.Append(arg.ToCSharpString()); + } + } + + withAttributes.Append(')'); + } + } + + if (withAttributes.Length > 0) + { + withAttributes.Append("] "); + withAttributes.Append(typeParameter.Name); + metadata.NameWithAttributes = withAttributes.ToString(); + } + + pb.SetMetadata(metadata.Build()); + + pb.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.ComponentTypeParameter, + typeParameter.Name, + builder.Name)); + }); + + static bool TryGetWhereClauseText(ITypeParameterSymbol typeParameter, PooledList constraints, [NotNullWhen(true)] out string? constraintsText) + { + if (constraints.Count == 0) + { + constraintsText = null; + return false; + } + + using var _ = StringBuilderPool.GetPooledObject(out var builder); + + builder.Append("where "); + builder.Append(typeParameter.Name); + builder.Append(" : "); + + var addComma = false; + + foreach (var item in constraints) + { + if (addComma) + { + builder.Append(", "); + } + else + { + addComma = true; + } + + builder.Append(item); + } + + constraintsText = builder.ToString(); + return true; + } + } + + private static TagHelperDescriptor CreateChildContentDescriptor(TagHelperDescriptor component, BoundAttributeDescriptor attribute) + { + var typeName = component.TypeName + "." + attribute.Name; + var assemblyName = component.AssemblyName; + + using var _ = TagHelperDescriptorBuilder.GetPooledInstance( + TagHelperKind.ChildContent, typeName, assemblyName, + out var builder); + + builder.SetTypeName(typeName, component.TypeNamespace, component.TypeNameIdentifier); + + builder.CaseSensitive = true; + + var xml = attribute.Documentation; + if (!string.IsNullOrEmpty(xml)) + { + builder.SetDocumentation(xml); + } + + // Child content matches the property name, but only as a direct child of the component. + builder.TagMatchingRule(r => + { + r.TagName = attribute.Name; + r.ParentTag = component.TagMatchingRules[0].TagName; + }); + + if (attribute.IsParameterizedChildContentProperty()) + { + // For child content attributes with a parameter, synthesize an attribute that allows you to name + // the parameter. + CreateContextParameter(builder, attribute.Name); + } + + if (component.IsFullyQualifiedNameMatch) + { + builder.IsFullyQualifiedNameMatch = true; + } + + var descriptor = builder.Build(); + + return descriptor; + } + + private static void CreateContextParameter(TagHelperDescriptorBuilder builder, string? childContentName) + { + builder.BindAttribute(b => + { + b.Name = ComponentHelpers.ChildContent.ParameterAttributeName; + b.TypeName = typeof(string).FullName; + b.PropertyName = b.Name; + b.SetMetadata(ChildContentParameterMetadata.Default); + + var documentation = childContentName == null + ? DocumentationDescriptor.ChildContentParameterName_TopLevel + : DocumentationDescriptor.From(DocumentationId.ChildContentParameterName, childContentName); + + b.SetDocumentation(documentation); + }); + } + + // Does a walk up the inheritance chain to determine the set of parameters by using + // a dictionary keyed on property name. + // + // We consider parameters to be defined by properties satisfying all of the following: + // - are public + // - are visible (not shadowed) + // - have the [Parameter] attribute + // - have a setter, even if private + // - are not indexers + private static ImmutableArray<(IPropertySymbol property, PropertyKind kind)> GetProperties(INamedTypeSymbol type) + { + using var names = new PooledHashSet(StringComparer.Ordinal); + using var results = new PooledArrayBuilder<(IPropertySymbol, PropertyKind)>(); + + var currentType = type; + do + { + if (currentType.HasFullName(ComponentsApi.ComponentBase.MetadataName)) + { + // The ComponentBase base class doesn't have any [Parameter]. + // Bail out now to avoid walking through its many members, plus the members + // of the System.Object base class. + break; + } + + foreach (var member in currentType.GetMembers()) + { + if (member is not IPropertySymbol property) + { + // Not a property + continue; + } + + if (names.Contains(property.Name)) + { + // Not visible + continue; + } + + var kind = PropertyKind.Default; + if (property.DeclaredAccessibility != Accessibility.Public) + { + // Not public + kind = PropertyKind.Ignored; + } + + if (property.Parameters.Length != 0) + { + // Indexer + kind = PropertyKind.Ignored; + } + + if (property.SetMethod == null) + { + // No setter + kind = PropertyKind.Ignored; + } + else if (property.SetMethod.DeclaredAccessibility != Accessibility.Public) + { + // No public setter + kind = PropertyKind.Ignored; + } + + if (property.IsStatic) + { + kind = PropertyKind.Ignored; + } + + if (!property.GetAttributes().Any(static a => a.HasFullName(ComponentsApi.ParameterAttribute.MetadataName))) + { + if (property.IsOverride) + { + // This property does not contain [Parameter] attribute but it was overridden. Don't ignore it for now. + // We can ignore it if the base class does not contains a [Parameter] as well. + continue; + } + + // Does not have [Parameter] + kind = PropertyKind.Ignored; + } + + if (kind == PropertyKind.Default) + { + kind = property switch + { + var p when IsEnum(p) => PropertyKind.Enum, + var p when IsRenderFragment(p) => PropertyKind.ChildContent, + var p when IsEventCallback(p) => PropertyKind.EventCallback, + var p when IsDelegate(p) => PropertyKind.Delegate, + _ => PropertyKind.Default + }; + } + + names.Add(property.Name); + results.Add((property, kind)); + } + + currentType = currentType.BaseType; + } + while (currentType != null); + + return results.ToImmutableAndClear(); + + static bool IsEnum(IPropertySymbol property) + { + return property.Type.TypeKind == TypeKind.Enum; + } + + static bool IsRenderFragment(IPropertySymbol property) + { + return property.Type.HasFullName(ComponentsApi.RenderFragment.MetadataName) || + (property.Type is INamedTypeSymbol { IsGenericType: true } namedType && + namedType.ConstructedFrom.HasFullName(ComponentsApi.RenderFragmentOfT.DisplayName)); + } + + static bool IsEventCallback(IPropertySymbol property) + { + return property.Type.HasFullName(ComponentsApi.EventCallback.MetadataName) || + (property.Type is INamedTypeSymbol { IsGenericType: true } namedType && + namedType.ConstructedFrom.HasFullName(ComponentsApi.EventCallbackOfT.DisplayName)); + } + + static bool IsDelegate(IPropertySymbol property) + { + return property.Type.TypeKind == TypeKind.Delegate; + } + } + + private static bool HasRenderModeDirective(INamedTypeSymbol type) + { + var attributes = type.GetAttributes(); + foreach (var attribute in attributes) + { + var attributeClass = attribute.AttributeClass; + while (attributeClass is not null) + { + if (attributeClass.HasFullName(ComponentsApi.RenderModeAttribute.FullTypeName)) + { + return true; + } + + attributeClass = attributeClass.BaseType; + } + } + return false; + } + + private enum PropertyKind + { + Ignored, + Default, + Enum, + ChildContent, + Delegate, + EventCallback, + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/DefaultTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/DefaultTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..4915952bee0 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/DefaultTagHelperProducer.Factory.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class DefaultTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(TagHelperTypes.ITagHelper, out var iTagHelperType) || + iTagHelperType.TypeKind == TypeKind.Error) + { + // If we can't find ITagHelper, then just bail. We won't discover anything. + result = null; + return false; + } + + var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation, excludeHidden); + + result = new DefaultTagHelperProducer(factory, iTagHelperType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/DefaultTagHelperProducer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/DefaultTagHelperProducer.cs new file mode 100644 index 00000000000..17dccc5e279 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/DefaultTagHelperProducer.cs @@ -0,0 +1,38 @@ +// 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; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class DefaultTagHelperProducer : TagHelperProducer +{ + private readonly DefaultTagHelperDescriptorFactory _factory; + private readonly INamedTypeSymbol _iTagHelperType; + + private DefaultTagHelperProducer(DefaultTagHelperDescriptorFactory factory, INamedTypeSymbol iTagHelperType) + { + _factory = factory; + _iTagHelperType = iTagHelperType; + } + + public override TagHelperProducerKind Kind => TagHelperProducerKind.Default; + + public override bool SupportsTypes => true; + + public override bool IsCandidateType(INamedTypeSymbol type) + => type.IsTagHelper(_iTagHelperType); + + public override void AddTagHelpersForType( + INamedTypeSymbol type, + ref TagHelperCollection.RefBuilder results, + CancellationToken cancellationToken) + { + if (_factory.CreateDescriptor(type) is { } descriptor) + { + results.Add(descriptor); + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/EventHandlerTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/EventHandlerTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..558c1be9325 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/EventHandlerTagHelperProducer.Factory.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class EventHandlerTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ComponentsApi.EventHandlerAttribute.FullTypeName, out var eventHandlerAttributeType)) + { + // If we can't find EventHandlerAttribute, then just bail. We won't discover anything. + result = null; + return false; + } + + result = new EventHandlerTagHelperProducer(eventHandlerAttributeType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/EventHandlerTagHelperProducer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/EventHandlerTagHelperProducer.cs new file mode 100644 index 00000000000..86ae0ab6a53 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/EventHandlerTagHelperProducer.cs @@ -0,0 +1,237 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class EventHandlerTagHelperProducer : TagHelperProducer +{ + private readonly INamedTypeSymbol _eventHandlerAttributeType; + + private EventHandlerTagHelperProducer(INamedTypeSymbol eventHandlerAttributeType) + { + _eventHandlerAttributeType = eventHandlerAttributeType; + } + + public override TagHelperProducerKind Kind => TagHelperProducerKind.EventHandler; + + public override bool SupportsTypes => true; + + public override bool IsCandidateType(INamedTypeSymbol type) + => type.DeclaredAccessibility == Accessibility.Public && + type.Name == "EventHandlers"; + + public override void AddTagHelpersForType( + INamedTypeSymbol type, + ref TagHelperCollection.RefBuilder results, + CancellationToken cancellationToken) + { + // Not handling duplicates here for now since we're the primary ones extending this. + // If we see users adding to the set of event handler constructs we will want to add deduplication + // and potentially diagnostics. + foreach (var attribute in type.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _eventHandlerAttributeType)) + { + if (!AttributeArgs.TryGet(attribute, out var args)) + { + // If this occurs, the [EventHandler] was defined incorrectly, so we can't create a tag helper. + continue; + } + + var typeName = type.GetDefaultDisplayString(); + var namespaceName = type.ContainingNamespace.GetFullName(); + results.Add(CreateTagHelper(typeName, namespaceName, type.Name, args)); + } + } + } + + + private readonly record struct AttributeArgs( + string Attribute, + INamedTypeSymbol EventArgsType, + bool EnableStopPropagation = false, + bool EnablePreventDefault = false) + { + public static bool TryGet(AttributeData attribute, out AttributeArgs args) + { + // EventHandlerAttribute has two constructors: + // + // - EventHandlerAttribute(string attributeName, Type eventArgsType); + // - EventHandlerAttribute(string attributeName, Type eventArgsType, bool enableStopPropagation, bool enablePreventDefault); + + var arguments = attribute.ConstructorArguments; + + return TryGetFromTwoArguments(arguments, out args) || + TryGetFromFourArguments(arguments, out args); + + static bool TryGetFromTwoArguments(ImmutableArray arguments, out AttributeArgs args) + { + // Ctor 1: EventHandlerAttribute(string attributeName, Type eventArgsType); + + if (arguments is [ + { Value: string attributeName }, + { Value: INamedTypeSymbol eventArgsType }]) + { + args = new(attributeName, eventArgsType); + return true; + } + + args = default; + return false; + } + + static bool TryGetFromFourArguments(ImmutableArray arguments, out AttributeArgs args) + { + // Ctor 2: EventHandlerAttribute(string attributeName, Type eventArgsType, bool enableStopPropagation, bool enablePreventDefault); + + // TODO: The enablePreventDefault and enableStopPropagation arguments are incorrectly swapped! + // However, they have been that way since the 4-argument constructor variant was introduced + // in https://github.com/dotnet/razor/commit/7635bba6ef2d3e6798d0846ceb96da6d5908e1b0. + // Fixing this is tracked be https://github.com/dotnet/razor/issues/10497 + + if (arguments is [ + { Value: string attributeName }, + { Value: INamedTypeSymbol eventArgsType }, + { Value: bool enablePreventDefault }, + { Value: bool enableStopPropagation }]) + { + args = new(attributeName, eventArgsType, enableStopPropagation, enablePreventDefault); + return true; + } + + args = default; + return false; + } + } + } + + private static TagHelperDescriptor CreateTagHelper( + string typeName, + string typeNamespace, + string typeNameIdentifier, + AttributeArgs args) + { + var (attribute, eventArgsType, enableStopPropagation, enablePreventDefault) = args; + + var attributeName = "@" + attribute; + var eventArgType = eventArgsType.GetDefaultDisplayString(); + using var _ = TagHelperDescriptorBuilder.GetPooledInstance( + TagHelperKind.EventHandler, attribute, ComponentsApi.AssemblyName, + out var builder); + + builder.SetTypeName(typeName, typeNamespace, typeNameIdentifier); + + builder.CaseSensitive = true; + builder.ClassifyAttributesOnly = true; + builder.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.EventHandlerTagHelper, + attributeName, + eventArgType)); + + builder.SetMetadata(new EventHandlerMetadata() + { + EventArgsType = eventArgType + }); + + builder.TagMatchingRule(rule => + { + rule.TagName = "*"; + + rule.Attribute(a => + { + a.Name = attributeName; + a.NameComparison = RequiredAttributeNameComparison.FullMatch; + a.IsDirectiveAttribute = true; + }); + }); + + if (enablePreventDefault) + { + builder.TagMatchingRule(rule => + { + rule.TagName = "*"; + + rule.Attribute(a => + { + a.Name = attributeName + ":preventDefault"; + a.NameComparison = RequiredAttributeNameComparison.FullMatch; + a.IsDirectiveAttribute = true; + }); + }); + } + + if (enableStopPropagation) + { + builder.TagMatchingRule(rule => + { + rule.TagName = "*"; + + rule.Attribute(a => + { + a.Name = attributeName + ":stopPropagation"; + a.NameComparison = RequiredAttributeNameComparison.FullMatch; + a.IsDirectiveAttribute = true; + }); + }); + } + + builder.BindAttribute(a => + { + a.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.EventHandlerTagHelper, + attributeName, + eventArgType)); + + a.Name = attributeName; + + // We want event handler directive attributes to default to C# context. + a.TypeName = $"Microsoft.AspNetCore.Components.EventCallback<{eventArgType}>"; + + a.PropertyName = attribute; + + a.IsDirectiveAttribute = true; + + // Make this weakly typed (don't type check) - delegates have their own type-checking + // logic that we don't want to interfere with. + a.IsWeaklyTyped = true; + + if (enablePreventDefault) + { + a.BindAttributeParameter(parameter => + { + parameter.Name = "preventDefault"; + parameter.PropertyName = "PreventDefault"; + parameter.TypeName = typeof(bool).FullName; + parameter.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.EventHandlerTagHelper_PreventDefault, + attributeName)); + }); + } + + if (enableStopPropagation) + { + a.BindAttributeParameter(parameter => + { + parameter.Name = "stopPropagation"; + parameter.PropertyName = "StopPropagation"; + parameter.TypeName = typeof(bool).FullName; + parameter.SetDocumentation( + DocumentationDescriptor.From( + DocumentationId.EventHandlerTagHelper_StopPropagation, + attributeName)); + }); + } + }); + + return builder.Build(); + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/FormNameTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/FormNameTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..721a89a5848 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/FormNameTagHelperProducer.Factory.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class FormNameTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + var renderTreeBuilderTypes = compilation.GetTypesByMetadataName(ComponentsApi.RenderTreeBuilder.FullTypeName) + .Where(IsValidRenderTreeBuilder) + .Take(2) + .ToArray(); + + if (renderTreeBuilderTypes is not [var renderTreeBuilderType]) + { + // If we can't find RenderTreeBuilder, then just bail. We won't be able to compile the generated code anyway. + result = null; + return false; + } + + result = new FormNameTagHelperProducer(renderTreeBuilderType); + return true; + + static bool IsValidRenderTreeBuilder(INamedTypeSymbol type) + { + return type.DeclaredAccessibility == Accessibility.Public && + type.GetMembers(ComponentsApi.RenderTreeBuilder.AddNamedEvent) + .Any(static m => m.DeclaredAccessibility == Accessibility.Public); + } + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/FormNameTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/FormNameTagHelperProducer.cs similarity index 55% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/FormNameTagHelperDescriptorProvider.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/FormNameTagHelperProducer.cs index b5ee133963c..82a41b38f30 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/FormNameTagHelperDescriptorProvider.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/FormNameTagHelperProducer.cs @@ -2,47 +2,35 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Linq; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Razor; +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; -// Run after the component tag helper provider -internal sealed class FormNameTagHelperDescriptorProvider() : TagHelperDescriptorProviderBase(order: 1000) +internal sealed partial class FormNameTagHelperProducer : TagHelperProducer { private static readonly Lazy s_formNameTagHelper = new(CreateFormNameTagHelper); - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) - { - ArgHelper.ThrowIfNull(context); + private readonly INamedTypeSymbol _renderTreeBuilderType; - var targetAssembly = context.TargetAssembly; - if (targetAssembly is not null && targetAssembly.Name != ComponentsApi.AssemblyName) - { - return; - } + private FormNameTagHelperProducer(INamedTypeSymbol renderTreeBuilderType) + { + _renderTreeBuilderType = renderTreeBuilderType; + } - var compilation = context.Compilation; + public override TagHelperProducerKind Kind => TagHelperProducerKind.FormName; - var renderTreeBuilders = compilation.GetTypesByMetadataName(ComponentsApi.RenderTreeBuilder.FullTypeName) - .Where(static t => t.DeclaredAccessibility == Accessibility.Public && - t.GetMembers(ComponentsApi.RenderTreeBuilder.AddNamedEvent).Any(static m => m.DeclaredAccessibility == Accessibility.Public)) - .Take(2).ToArray(); - if (renderTreeBuilders is not [var renderTreeBuilder]) - { - return; - } + public override bool SupportsStaticTagHelpers => true; - if (targetAssembly is not null && - !SymbolEqualityComparer.Default.Equals(targetAssembly, renderTreeBuilder.ContainingAssembly)) + public override void AddStaticTagHelpers(IAssemblySymbol assembly, ref TagHelperCollection.RefBuilder results) + { + if (assembly.Name != ComponentsApi.AssemblyName && + !SymbolEqualityComparer.Default.Equals(assembly, _renderTreeBuilderType.ContainingAssembly)) { return; } - context.Results.Add(s_formNameTagHelper.Value); + results.Add(s_formNameTagHelper.Value); } private static TagHelperDescriptor CreateFormNameTagHelper() diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ITagHelperProducerFactory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ITagHelperProducerFactory.cs new file mode 100644 index 00000000000..d20bbf2d508 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ITagHelperProducerFactory.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal interface ITagHelperProducerFactory : IRazorEngineFeature +{ + bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result); +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/KeyTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/KeyTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..5deee689959 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/KeyTagHelperProducer.Factory.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class KeyTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ComponentsApi.RenderTreeBuilder.FullTypeName, out var renderTreeBuilderType)) + { + // If we can't find RenderTreeBuilder, then just bail. We won't be able to compile the generated code anyway. + result = null; + return false; + } + + result = new KeyTagHelperProducer(renderTreeBuilderType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/KeyTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/KeyTagHelperProducer.cs similarity index 59% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/KeyTagHelperDescriptorProvider.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/KeyTagHelperProducer.cs index 832a2e12894..29aa9df0e35 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/KeyTagHelperDescriptorProvider.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/KeyTagHelperProducer.cs @@ -2,39 +2,34 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Razor; +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; -// Run after the component tag helper provider -internal sealed class KeyTagHelperDescriptorProvider() : TagHelperDescriptorProviderBase(order: 1000) +internal sealed partial class KeyTagHelperProducer : TagHelperProducer { private static readonly Lazy s_keyTagHelper = new(CreateKeyTagHelper); - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) + private readonly INamedTypeSymbol _renderTreeBuilderType; + + private KeyTagHelperProducer(INamedTypeSymbol renderTreeBuilderType) { - ArgHelper.ThrowIfNull(context); + _renderTreeBuilderType = renderTreeBuilderType; + } - var compilation = context.Compilation; + public override TagHelperProducerKind Kind => TagHelperProducerKind.Key; - var renderTreeBuilderType = compilation.GetTypeByMetadataName(ComponentsApi.RenderTreeBuilder.FullTypeName); - if (renderTreeBuilderType == null) - { - // If we can't find RenderTreeBuilder, then just bail. We won't be able to compile the - // generated code anyway. - return; - } + public override bool SupportsStaticTagHelpers => true; - if (context.TargetAssembly is { } targetAssembly && - !SymbolEqualityComparer.Default.Equals(targetAssembly, renderTreeBuilderType.ContainingAssembly)) + public override void AddStaticTagHelpers(IAssemblySymbol assembly, ref TagHelperCollection.RefBuilder results) + { + if (!SymbolEqualityComparer.Default.Equals(assembly, _renderTreeBuilderType.ContainingAssembly)) { return; } - context.Results.Add(s_keyTagHelper.Value); + results.Add(s_keyTagHelper.Value); } private static TagHelperDescriptor CreateKeyTagHelper() diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RefTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RefTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..c50997707d8 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RefTagHelperProducer.Factory.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class RefTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ComponentsApi.ElementReference.FullTypeName, out var elementReferenceType)) + { + // If we can't find ElementRef, then just bail. We won't be able to compile the generated code anyway. + result = null; + return false; + } + + result = new RefTagHelperProducer(elementReferenceType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/RefTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RefTagHelperProducer.cs similarity index 58% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/RefTagHelperDescriptorProvider.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RefTagHelperProducer.cs index 4a4d58c9616..17950257f52 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/RefTagHelperDescriptorProvider.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RefTagHelperProducer.cs @@ -2,39 +2,34 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Razor; +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; -// Run after the component tag helper provider, because later we may want component-type-specific variants of this -internal sealed class RefTagHelperDescriptorProvider() : TagHelperDescriptorProviderBase(order: 1000) +internal sealed partial class RefTagHelperProducer : TagHelperProducer { private static readonly Lazy s_refTagHelper = new(CreateRefTagHelper); - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) + private readonly INamedTypeSymbol _elementReferenceType; + + private RefTagHelperProducer(INamedTypeSymbol elementReferenceType) { - ArgHelper.ThrowIfNull(context); + _elementReferenceType = elementReferenceType; + } - var compilation = context.Compilation; + public override TagHelperProducerKind Kind => TagHelperProducerKind.Ref; - var elementReference = compilation.GetTypeByMetadataName(ComponentsApi.ElementReference.FullTypeName); - if (elementReference == null) - { - // If we can't find ElementRef, then just bail. We won't be able to compile the - // generated code anyway. - return; - } + public override bool SupportsStaticTagHelpers => true; - if (context.TargetAssembly is { } targetAssembly && - !SymbolEqualityComparer.Default.Equals(targetAssembly, elementReference.ContainingAssembly)) + public override void AddStaticTagHelpers(IAssemblySymbol assembly, ref TagHelperCollection.RefBuilder results) + { + if (!SymbolEqualityComparer.Default.Equals(assembly, _elementReferenceType.ContainingAssembly)) { return; } - context.Results.Add(s_refTagHelper.Value); + results.Add(s_refTagHelper.Value); } private static TagHelperDescriptor CreateRefTagHelper() diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RenderModeTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RenderModeTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..bc3dd3891ed --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RenderModeTagHelperProducer.Factory.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class RenderModeTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ComponentsApi.IComponentRenderMode.FullTypeName, out var iComponentRenderModeType)) + { + // If we can't find IComponentRenderMode, then just bail. We won't be able to compile the + // generated code anyway. + result = null; + return false; + } + + result = new RenderModeTagHelperProducer(iComponentRenderModeType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/RenderModeTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RenderModeTagHelperProducer.cs similarity index 61% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/RenderModeTagHelperDescriptorProvider.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RenderModeTagHelperProducer.cs index 393fcca4e5c..b02b318cbc3 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/RenderModeTagHelperDescriptorProvider.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/RenderModeTagHelperProducer.cs @@ -2,39 +2,34 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Razor; +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; -// Run after the component tag helper provider -internal sealed class RenderModeTagHelperDescriptorProvider() : TagHelperDescriptorProviderBase(order: 1000) +internal sealed partial class RenderModeTagHelperProducer : TagHelperProducer { private static readonly Lazy s_renderModeTagHelper = new(CreateRenderModeTagHelper); - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) + private readonly INamedTypeSymbol _iComponentRenderModeType; + + private RenderModeTagHelperProducer(INamedTypeSymbol iComponentRenderModeType) { - ArgHelper.ThrowIfNull(context); + _iComponentRenderModeType = iComponentRenderModeType; + } - var compilation = context.Compilation; + public override TagHelperProducerKind Kind => TagHelperProducerKind.RenderMode; - var iComponentRenderMode = compilation.GetTypeByMetadataName(ComponentsApi.IComponentRenderMode.FullTypeName); - if (iComponentRenderMode == null) - { - // If we can't find IComponentRenderMode, then just bail. We won't be able to compile the - // generated code anyway. - return; - } + public override bool SupportsStaticTagHelpers => true; - if (context.TargetAssembly is { } targetAssembly && - !SymbolEqualityComparer.Default.Equals(targetAssembly, iComponentRenderMode.ContainingAssembly)) + public override void AddStaticTagHelpers(IAssemblySymbol assembly, ref TagHelperCollection.RefBuilder results) + { + if (!SymbolEqualityComparer.Default.Equals(assembly, _iComponentRenderModeType.ContainingAssembly)) { return; } - context.Results.Add(s_renderModeTagHelper.Value); + results.Add(s_renderModeTagHelper.Value); } private static TagHelperDescriptor CreateRenderModeTagHelper() diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/SplatTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/SplatTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..7fb8dd8fef7 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/SplatTagHelperProducer.Factory.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal sealed partial class SplatTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ComponentsApi.RenderTreeBuilder.FullTypeName, out var renderTreeBuilderType)) + { + // If we can't find RenderTreeBuilder, then just bail. We won't be able to compile the generated code anyway. + result = null; + return false; + } + + result = new SplatTagHelperProducer(renderTreeBuilderType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/SplatTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/SplatTagHelperProducer.cs similarity index 62% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/SplatTagHelperDescriptorProvider.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/SplatTagHelperProducer.cs index 945f8587298..85db8147aaf 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/SplatTagHelperDescriptorProvider.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/SplatTagHelperProducer.cs @@ -2,38 +2,34 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Razor; +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; -internal sealed class SplatTagHelperDescriptorProvider : TagHelperDescriptorProviderBase +internal sealed partial class SplatTagHelperProducer : TagHelperProducer { private static readonly Lazy s_splatTagHelper = new(CreateSplatTagHelper); - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) + private readonly INamedTypeSymbol _renderTreeBuilderType; + + private SplatTagHelperProducer(INamedTypeSymbol renderTreeBuilderType) { - ArgHelper.ThrowIfNull(context); + _renderTreeBuilderType = renderTreeBuilderType; + } - var compilation = context.Compilation; + public override TagHelperProducerKind Kind => TagHelperProducerKind.Splat; - var renderTreeBuilder = compilation.GetTypeByMetadataName(ComponentsApi.RenderTreeBuilder.FullTypeName); - if (renderTreeBuilder == null) - { - // If we can't find RenderTreeBuilder, then just bail. We won't be able to compile the - // generated code anyway. - return; - } + public override bool SupportsStaticTagHelpers => true; - if (context.TargetAssembly is { } targetAssembly && - !SymbolEqualityComparer.Default.Equals(targetAssembly, renderTreeBuilder.ContainingAssembly)) + public override void AddStaticTagHelpers(IAssemblySymbol assembly, ref TagHelperCollection.RefBuilder results) + { + if (!SymbolEqualityComparer.Default.Equals(assembly, _renderTreeBuilderType.ContainingAssembly)) { return; } - context.Results.Add(s_splatTagHelper.Value); + results.Add(s_splatTagHelper.Value); } private static TagHelperDescriptor CreateSplatTagHelper() diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/TagHelperProducer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/TagHelperProducer.cs new file mode 100644 index 00000000000..6925a6fb271 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/TagHelperProducer.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal abstract class TagHelperProducer +{ + public abstract class FactoryBase : RazorEngineFeatureBase, IRazorEngineFeature, ITagHelperProducerFactory + { + public bool TryCreate(Compilation compilation, [NotNullWhen(true)] out TagHelperProducer? result) + => TryCreate(compilation, includeDocumentation: false, excludeHidden: false, out result); + + public abstract bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result); + } + + public abstract TagHelperProducerKind Kind { get; } + + public virtual bool SupportsStaticTagHelpers => false; + + public virtual void AddStaticTagHelpers(IAssemblySymbol assembly, ref TagHelperCollection.RefBuilder results) + { + } + + public virtual bool SupportsTypes => false; + + public virtual bool SupportsNestedTypes => false; + + public virtual bool IsCandidateType(INamedTypeSymbol type) => false; + + public virtual void AddTagHelpersForType( + INamedTypeSymbol type, + ref TagHelperCollection.RefBuilder results, + CancellationToken cancellationToken) + { + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/TagHelperProducerKind.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/TagHelperProducerKind.cs new file mode 100644 index 00000000000..6d9fca39b12 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/TagHelperProducerKind.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; + +internal enum TagHelperProducerKind : ushort +{ + Default = 0, + Bind, + Component, + EventHandler, + FormName, + Key, + Ref, + RenderMode, + Splat, + MvcViewComponent, + Mvc1_X_ViewComponent, + Mvc2_X_ViewComponent +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/RoslynExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/RoslynExtensions.cs new file mode 100644 index 00000000000..025c4ed4f58 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/RoslynExtensions.cs @@ -0,0 +1,22 @@ +// 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 System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Language.TagHelpers; + +internal static class RoslynExtensions +{ + public static bool TryGetTypeByMetadataName( + this Compilation compilation, + string fullyQualifiedMetadataName, + [NotNullWhen(true)] out INamedTypeSymbol? result) + { + result = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); + return result is not null; + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/RazorExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/RazorExtensions.cs index 153c0d78b0d..cbb891b4156 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/RazorExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/RazorExtensions.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Razor; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; @@ -15,17 +13,14 @@ public static class RazorExtensions { public static void Register(RazorProjectEngineBuilder builder) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + ArgHelper.ThrowIfNull(builder); InjectDirective.Register(builder, considerNullabilityEnforcement: false); ModelDirective.Register(builder); InheritsDirective.Register(builder); - builder.Features.Add(new DefaultTagHelperDescriptorProvider()); + builder.Features.Add(new DefaultTagHelperProducer.Factory()); // Register section directive with the 1.x compatible target extension. builder.AddDirective(SectionDirective.Directive); @@ -48,12 +43,9 @@ public static void Register(RazorProjectEngineBuilder builder) public static void RegisterViewComponentTagHelpers(RazorProjectEngineBuilder builder) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + ArgHelper.ThrowIfNull(builder); - builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + builder.Features.Add(new ViewComponentTagHelperProducer.Factory()); builder.Features.Add(new ViewComponentTagHelperPass()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperDescriptorProvider.cs deleted file mode 100644 index e411ec8aeb9..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperDescriptorProvider.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; - -public sealed class ViewComponentTagHelperDescriptorProvider : TagHelperDescriptorProviderBase -{ - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) - { - ArgHelper.ThrowIfNull(context); - - var compilation = context.Compilation; - - var vcAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute); - var nonVCAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); - if (vcAttribute == null || vcAttribute.TypeKind == TypeKind.Error) - { - // Could not find attributes we care about in the compilation. Nothing to do. - return; - } - - var factory = new ViewComponentTagHelperDescriptorFactory(compilation); - var collector = new Collector(compilation, factory, vcAttribute, nonVCAttribute); - - collector.Collect(context, cancellationToken); - } - - private class Collector( - Compilation compilation, - ViewComponentTagHelperDescriptorFactory factory, - INamedTypeSymbol vcAttribute, - INamedTypeSymbol? nonVCAttribute) - : TagHelperCollector(compilation, targetAssembly: null) - { - private readonly ViewComponentTagHelperDescriptorFactory _factory = factory; - private readonly INamedTypeSymbol _vcAttribute = vcAttribute; - private readonly INamedTypeSymbol? _nonVCAttribute = nonVCAttribute; - - protected override bool IncludeNestedTypes => true; - - protected override bool IsCandidateType(INamedTypeSymbol type) - => type.IsViewComponent(_vcAttribute, _nonVCAttribute); - - protected override void Collect( - INamedTypeSymbol type, - ICollection results, - CancellationToken cancellationToken) - { - var descriptor = _factory.CreateDescriptor(type); - - if (descriptor != null) - { - results.Add(descriptor); - } - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..ea0082fe3d0 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperProducer.Factory.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.TagHelpers; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; + +internal sealed partial class ViewComponentTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute, out var viewComponentAttributeType) || + viewComponentAttributeType.TypeKind == TypeKind.Error) + { + result = null; + return false; + } + + var nonViewComponentAttributeType = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); + + var factory = new ViewComponentTagHelperDescriptorFactory(compilation); + result = new ViewComponentTagHelperProducer(factory, viewComponentAttributeType, nonViewComponentAttributeType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperProducer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperProducer.cs new file mode 100644 index 00000000000..6d4215197d0 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/ViewComponentTagHelperProducer.cs @@ -0,0 +1,45 @@ +// 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; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; + +internal sealed partial class ViewComponentTagHelperProducer : TagHelperProducer +{ + private readonly ViewComponentTagHelperDescriptorFactory _factory; + private readonly INamedTypeSymbol _viewComponentAttributeType; + private readonly INamedTypeSymbol? _nonViewComponentAttributeType; + + private ViewComponentTagHelperProducer( + ViewComponentTagHelperDescriptorFactory factory, + INamedTypeSymbol viewComponentAttributeType, + INamedTypeSymbol? nonViewComponentAttributeType) + { + _factory = factory; + _viewComponentAttributeType = viewComponentAttributeType; + _nonViewComponentAttributeType = nonViewComponentAttributeType; + } + + public override TagHelperProducerKind Kind => TagHelperProducerKind.Mvc1_X_ViewComponent; + + public override bool SupportsTypes => true; + public override bool SupportsNestedTypes => true; + + public override bool IsCandidateType(INamedTypeSymbol type) + => type.IsViewComponent(_viewComponentAttributeType, _nonViewComponentAttributeType); + + public override void AddTagHelpersForType( + INamedTypeSymbol type, + ref TagHelperCollection.RefBuilder results, + CancellationToken cancellationToken) + { + if (_factory.CreateDescriptor(type) is { } descriptor) + { + results.Add(descriptor); + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/RazorExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/RazorExtensions.cs index 31d4a578270..5452130a4dd 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/RazorExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/RazorExtensions.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Razor; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; @@ -15,10 +13,7 @@ public static class RazorExtensions { public static void Register(RazorProjectEngineBuilder builder) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + ArgHelper.ThrowIfNull(builder); FunctionsDirective.Register(builder); InjectDirective.Register(builder, considerNullabilityEnforcement: false); @@ -29,8 +24,8 @@ public static void Register(RazorProjectEngineBuilder builder) InheritsDirective.Register(builder); SectionDirective.Register(builder); - builder.Features.Add(new DefaultTagHelperDescriptorProvider()); - builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + builder.Features.Add(new DefaultTagHelperProducer.Factory()); + builder.Features.Add(new ViewComponentTagHelperProducer.Factory()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); builder.AddTargetExtension(new TemplateTargetExtension() diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperDescriptorProvider.cs deleted file mode 100644 index 3423015b5af..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperDescriptorProvider.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; - -public sealed class ViewComponentTagHelperDescriptorProvider : TagHelperDescriptorProviderBase -{ - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) - { - ArgHelper.ThrowIfNull(context); - - var compilation = context.Compilation; - - var vcAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute); - var nonVCAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); - if (vcAttribute == null || vcAttribute.TypeKind == TypeKind.Error) - { - // Could not find attributes we care about in the compilation. Nothing to do. - return; - } - - var factory = new ViewComponentTagHelperDescriptorFactory(compilation); - var collector = new Collector(compilation, factory, vcAttribute, nonVCAttribute); - - collector.Collect(context, cancellationToken); - } - - private class Collector( - Compilation compilation, - ViewComponentTagHelperDescriptorFactory factory, - INamedTypeSymbol vcAttribute, - INamedTypeSymbol? nonVCAttribute) - : TagHelperCollector(compilation, targetAssembly: null) - { - private readonly ViewComponentTagHelperDescriptorFactory _factory = factory; - private readonly INamedTypeSymbol _vcAttribute = vcAttribute; - private readonly INamedTypeSymbol? _nonVCAttribute = nonVCAttribute; - - protected override bool IncludeNestedTypes => true; - - protected override bool IsCandidateType(INamedTypeSymbol type) - => type.IsViewComponent(_vcAttribute, _nonVCAttribute); - - protected override void Collect( - INamedTypeSymbol type, - ICollection results, - CancellationToken cancellationToken) - { - var descriptor = _factory.CreateDescriptor(type); - - if (descriptor != null) - { - results.Add(descriptor); - } - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..1e9d4e9d28d --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperProducer.Factory.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.TagHelpers; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; + +internal sealed partial class ViewComponentTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute, out var viewComponentAttributeType) || + viewComponentAttributeType.TypeKind == TypeKind.Error) + { + result = null; + return false; + } + + var nonViewComponentAttributeType = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); + + var factory = new ViewComponentTagHelperDescriptorFactory(compilation); + result = new ViewComponentTagHelperProducer(factory, viewComponentAttributeType, nonViewComponentAttributeType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperProducer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperProducer.cs new file mode 100644 index 00000000000..aabf2e36852 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperProducer.cs @@ -0,0 +1,45 @@ +// 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; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; + +internal sealed partial class ViewComponentTagHelperProducer : TagHelperProducer +{ + private readonly ViewComponentTagHelperDescriptorFactory _factory; + private readonly INamedTypeSymbol _viewComponentAttributeType; + private readonly INamedTypeSymbol? _nonViewComponentAttributeType; + + private ViewComponentTagHelperProducer( + ViewComponentTagHelperDescriptorFactory factory, + INamedTypeSymbol viewComponentAttributeType, + INamedTypeSymbol? nonViewComponentAttributeType) + { + _factory = factory; + _viewComponentAttributeType = viewComponentAttributeType; + _nonViewComponentAttributeType = nonViewComponentAttributeType; + } + + public override TagHelperProducerKind Kind => TagHelperProducerKind.Mvc2_X_ViewComponent; + + public override bool SupportsTypes => true; + public override bool SupportsNestedTypes => true; + + public override bool IsCandidateType(INamedTypeSymbol type) + => type.IsViewComponent(_viewComponentAttributeType, _nonViewComponentAttributeType); + + public override void AddTagHelpersForType( + INamedTypeSymbol type, + ref TagHelperCollection.RefBuilder results, + CancellationToken cancellationToken) + { + if (_factory.CreateDescriptor(type) is { } descriptor) + { + results.Add(descriptor); + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorExtensions.cs index 317a5f387b0..027d6998f99 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorExtensions.cs @@ -1,15 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; -using System.Diagnostics; -using System.Threading; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Razor; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; @@ -17,10 +13,7 @@ public static class RazorExtensions { public static void Register(RazorProjectEngineBuilder builder) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + ArgHelper.ThrowIfNull(builder); InjectDirective.Register(builder, considerNullabilityEnforcement: true); ModelDirective.Register(builder); @@ -28,8 +21,8 @@ public static void Register(RazorProjectEngineBuilder builder) SectionDirective.Register(builder); - builder.Features.Add(new DefaultTagHelperDescriptorProvider()); - builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + builder.Features.Add(new DefaultTagHelperProducer.Factory()); + builder.Features.Add(new ViewComponentTagHelperProducer.Factory()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); builder.AddTargetExtension(new TemplateTargetExtension() diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperDescriptorProvider.cs deleted file mode 100644 index 5b75ed3cc0a..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperDescriptorProvider.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Threading; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; - -public sealed class ViewComponentTagHelperDescriptorProvider : TagHelperDescriptorProviderBase -{ - public override void Execute(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken = default) - { - ArgHelper.ThrowIfNull(context); - - var compilation = context.Compilation; - - var vcAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute); - var nonVCAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); - if (vcAttribute == null || vcAttribute.TypeKind == TypeKind.Error) - { - // Could not find attributes we care about in the compilation. Nothing to do. - return; - } - - var factory = new ViewComponentTagHelperDescriptorFactory(compilation); - var collector = new Collector(compilation, factory, vcAttribute, nonVCAttribute); - - collector.Collect(context, cancellationToken); - } - - private class Collector( - Compilation compilation, - ViewComponentTagHelperDescriptorFactory factory, - INamedTypeSymbol vcAttribute, - INamedTypeSymbol? nonVCAttribute) - : TagHelperCollector(compilation, targetAssembly: null) - { - private readonly ViewComponentTagHelperDescriptorFactory _factory = factory; - private readonly INamedTypeSymbol _vcAttribute = vcAttribute; - private readonly INamedTypeSymbol? _nonVCAttribute = nonVCAttribute; - - protected override bool IncludeNestedTypes => true; - - protected override bool IsCandidateType(INamedTypeSymbol type) - => type.IsViewComponent(_vcAttribute, _nonVCAttribute); - - protected override void Collect( - INamedTypeSymbol type, - ICollection results, - CancellationToken cancellationToken) - { - var descriptor = _factory.CreateDescriptor(type); - - if (descriptor != null) - { - results.Add(descriptor); - } - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperProducer.Factory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperProducer.Factory.cs new file mode 100644 index 00000000000..57d19df3724 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperProducer.Factory.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.TagHelpers; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; + +internal sealed partial class ViewComponentTagHelperProducer +{ + public sealed class Factory : FactoryBase + { + public override bool TryCreate( + Compilation compilation, + bool includeDocumentation, + bool excludeHidden, + [NotNullWhen(true)] out TagHelperProducer? result) + { + if (!compilation.TryGetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute, out var viewComponentAttributeType) || + viewComponentAttributeType.TypeKind == TypeKind.Error) + { + result = null; + return false; + } + + var nonViewComponentAttributeType = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); + + var factory = new ViewComponentTagHelperDescriptorFactory(compilation); + result = new ViewComponentTagHelperProducer(factory, viewComponentAttributeType, nonViewComponentAttributeType); + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperProducer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperProducer.cs new file mode 100644 index 00000000000..c3cbe2f3253 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperProducer.cs @@ -0,0 +1,45 @@ +// 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; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; + +internal sealed partial class ViewComponentTagHelperProducer : TagHelperProducer +{ + private readonly ViewComponentTagHelperDescriptorFactory _factory; + private readonly INamedTypeSymbol _viewComponentAttributeType; + private readonly INamedTypeSymbol? _nonViewComponentAttributeType; + + private ViewComponentTagHelperProducer( + ViewComponentTagHelperDescriptorFactory factory, + INamedTypeSymbol viewComponentAttributeType, + INamedTypeSymbol? nonViewComponentAttributeType) + { + _factory = factory; + _viewComponentAttributeType = viewComponentAttributeType; + _nonViewComponentAttributeType = nonViewComponentAttributeType; + } + + public override TagHelperProducerKind Kind => TagHelperProducerKind.MvcViewComponent; + + public override bool SupportsTypes => true; + public override bool SupportsNestedTypes => true; + + public override bool IsCandidateType(INamedTypeSymbol type) + => type.IsViewComponent(_viewComponentAttributeType, _nonViewComponentAttributeType); + + public override void AddTagHelpersForType( + INamedTypeSymbol type, + ref TagHelperCollection.RefBuilder results, + CancellationToken cancellationToken) + { + if (_factory.CreateDescriptor(type) is { } descriptor) + { + results.Add(descriptor); + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs index 1b8be17092e..7fa497aae62 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs @@ -80,11 +80,11 @@ private static StaticCompilationTagHelperFeature GetStaticTagHelperFeature(Compi { var tagHelperFeature = new StaticCompilationTagHelperFeature(compilation); - // the tagHelperFeature will have its Engine property set as part of adding it to the engine, which is used later when doing the actual discovery + // the tagHelperFeature will have its Engine property set as part of adding it to the engine, + // which is used later when doing the actual discovery var discoveryProjectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, new VirtualRazorProjectFileSystem(), b => { b.Features.Add(tagHelperFeature); - b.Features.Add(new DefaultTagHelperDescriptorProvider()); CompilerFeatures.Register(b); RazorExtensions.Register(b); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs index 66c6ca5d9b5..a9cf2656d7c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs @@ -133,13 +133,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStart(); var tagHelperFeature = GetStaticTagHelperFeature(compilation); - using var builder = new TagHelperCollection.Builder(); - - tagHelperFeature.CollectDescriptors(compilation.Assembly, builder, cancellationToken); + var collection = tagHelperFeature.GetTagHelpers(compilation.Assembly, cancellationToken); RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStop(); - return builder.ToCollection(); + return collection; }) .WithLambdaComparer(static (a, b) => a!.SequenceEqual(b!)); @@ -228,21 +226,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context) RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromReferencesStart(); var tagHelperFeature = GetStaticTagHelperFeature(compilation); - // Typically a project with Razor files will have many tag helpers in references. - // So, we start with a larger capacity to avoid extra array copies. - using var builder = new TagHelperCollection.Builder(); + using var collections = new MemoryBuilder(initialCapacity: 512, clearArray: true); foreach (var reference in compilation.References) { if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) { - tagHelperFeature.CollectDescriptors(assembly, builder, cancellationToken); + var collection = tagHelperFeature.GetTagHelpers(assembly, cancellationToken); + if (!collection.IsEmpty) + { + collections.Append(collection); + } } } RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromReferencesStop(); - return builder.ToCollection(); + return TagHelperCollection.Merge(collections.AsMemory().Span); }); var allTagHelpers = tagHelpersFromCompilation diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/StaticCompilationTagHelperFeature.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/StaticCompilationTagHelperFeature.cs index fb0bb071f0b..79d08eaad3d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/StaticCompilationTagHelperFeature.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/StaticCompilationTagHelperFeature.cs @@ -3,46 +3,47 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Threading; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; namespace Microsoft.NET.Sdk.Razor.SourceGenerators { - internal sealed class StaticCompilationTagHelperFeature(Compilation compilation) - : RazorEngineFeatureBase, ITagHelperFeature + internal sealed class StaticCompilationTagHelperFeature(Compilation compilation) : RazorEngineFeatureBase, ITagHelperFeature { - private ImmutableArray _providers; + private ITagHelperDiscoveryService? _discoveryService; + private TagHelperDiscoverer? _discoverer; - public void CollectDescriptors( - IAssemblySymbol? targetAssembly, - TagHelperCollection.Builder results, - CancellationToken cancellationToken) + public TagHelperCollection GetTagHelpers(IAssemblySymbol assembly, CancellationToken cancellationToken) { - if (_providers.IsDefaultOrEmpty) + if (_discoveryService is null) { - return; + return []; } - var context = new TagHelperDescriptorProviderContext(compilation, targetAssembly, results); - - foreach (var provider in _providers) + if (_discoverer is null && + !_discoveryService.TryGetDiscoverer(compilation, out _discoverer)) { - provider.Execute(context, cancellationToken); + return []; } + + return _discoverer.GetTagHelpers(assembly, cancellationToken); } TagHelperCollection ITagHelperFeature.GetTagHelpers(CancellationToken cancellationToken) { - using var builder = new TagHelperCollection.Builder(); - CollectDescriptors(targetAssembly: null, builder, cancellationToken); + if (_discoveryService is null) + { + return []; + } - return builder.ToCollection(); + return _discoveryService.GetTagHelpers(compilation, cancellationToken); } protected override void OnInitialized() { - _providers = Engine.GetFeatures().OrderByAsArray(static x => x.Order); + _discoveryService = Engine.GetFeatures().FirstOrDefault(); } } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs deleted file mode 100644 index 30567502e75..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Test.Utilities; -using Xunit; - -namespace Microsoft.CodeAnalysis.Razor; - -public abstract class TagHelperDescriptorProviderTestBase -{ - protected TagHelperDescriptorProviderTestBase(string additionalCodeOpt = null) - { - CSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp7_3); - var testTagHelpers = CSharpCompilation.Create( - assemblyName: AssemblyName, - syntaxTrees: - [ - Parse(TagHelperDescriptorFactoryTagHelpers.Code), - ..(additionalCodeOpt != null ? [Parse(additionalCodeOpt)] : Enumerable.Empty()), - ], - references: ReferenceUtil.AspNetLatestAll, - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - BaseCompilation = TestCompilation.Create( - syntaxTrees: [], - references: [testTagHelpers.VerifyDiagnostics().EmitToImageReference()]); - } - - protected Compilation BaseCompilation { get; } - - protected CSharpParseOptions CSharpParseOptions { get; } - - protected static string AssemblyName { get; } = "Microsoft.CodeAnalysis.Razor.Test"; - - protected CSharpSyntaxTree Parse(string text) - { - return (CSharpSyntaxTree)CSharpSyntaxTree.ParseText(text, CSharpParseOptions); - } - - // For simplicity in testing, exclude the built-in components. We'll add more and we - // don't want to update the tests when that happens. - protected static TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context) - { - var results = - context.Results - .Where(c => !c.DisplayName.StartsWith("Microsoft.AspNetCore.Components.", StringComparison.Ordinal)) - .OrderBy(c => c.Name) - .ToArray(); - - return results; - } - - protected static TagHelperDescriptor[] AssertAndExcludeFullyQualifiedNameMatchComponents( - TagHelperDescriptor[] components, - int expectedCount) - { - var componentLookup = new Dictionary>(); - var fullyQualifiedNameMatchComponents = components.Where(c => c.IsFullyQualifiedNameMatch).ToArray(); - Assert.Equal(expectedCount, fullyQualifiedNameMatchComponents.Length); - - var shortNameMatchComponents = components.Where(c => !c.IsFullyQualifiedNameMatch).ToArray(); - - // For every fully qualified name component, we want to make sure we have a corresponding short name component. - foreach (var fullNameComponent in fullyQualifiedNameMatchComponents) - { - Assert.Contains(shortNameMatchComponents, component => - { - return component.Name == fullNameComponent.Name && - component.Kind == fullNameComponent.Kind && - component.BoundAttributes.SequenceEqual(fullNameComponent.BoundAttributes); - }); - } - - return shortNameMatchComponents; - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperProducerTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperProducerTest.cs new file mode 100644 index 00000000000..42c632059a5 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperProducerTest.cs @@ -0,0 +1,131 @@ +// 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 System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor; + +public abstract class TagHelperDescriptorProviderTestBase +{ + protected TagHelperDescriptorProviderTestBase(string? additionalCode = null) + { + CSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp7_3); + + var testTagHelpers = CSharpCompilation.Create( + assemblyName: AssemblyName, + syntaxTrees: + [ + Parse(TagHelperDescriptorFactoryTagHelpers.Code), + .. additionalCode != null ? [Parse(additionalCode)] : Array.Empty(), + ], + references: ReferenceUtil.AspNetLatestAll, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + BaseCompilation = TestCompilation.Create( + syntaxTrees: [], + references: [testTagHelpers.VerifyDiagnostics().EmitToImageReference()]); + + var projectEngine = RazorProjectEngine.CreateEmpty(builder => + { + builder.Features.Add(new TagHelperDiscoveryService()); + + ConfigureEngine(builder); + }); + + Engine = projectEngine.Engine; + } + + protected RazorEngine Engine { get; } + + protected Compilation BaseCompilation { get; } + + protected CSharpParseOptions CSharpParseOptions { get; } + + protected static string AssemblyName { get; } = "Microsoft.CodeAnalysis.Razor.Test"; + + protected virtual void ConfigureEngine(RazorProjectEngineBuilder builder) + { + } + + private protected TagHelperCollection GetTagHelpers(Compilation compilation, TagHelperDiscoveryOptions options) + => GetDiscoveryService().GetTagHelpers(compilation, options); + + private protected TagHelperCollection GetTagHelpers(Compilation compilation) + => GetDiscoveryService().GetTagHelpers(compilation); + + private protected bool TryGetDiscoverer( + Compilation compilation, TagHelperDiscoveryOptions options, [NotNullWhen(true)] out TagHelperDiscoverer? discoverer) + => GetDiscoveryService().TryGetDiscoverer(compilation, options, out discoverer); + + private protected bool TryGetDiscoverer( + Compilation compilation, [NotNullWhen(true)] out TagHelperDiscoverer? discoverer) + => GetDiscoveryService().TryGetDiscoverer(compilation, out discoverer); + + private protected ITagHelperDiscoveryService GetDiscoveryService() + { + Assert.True(Engine.TryGetFeature(out ITagHelperDiscoveryService? discoveryService)); + return discoveryService; + } + + protected CSharpSyntaxTree Parse(string text) + { + return (CSharpSyntaxTree)CSharpSyntaxTree.ParseText(text, CSharpParseOptions); + } + + protected static bool IsBuiltInComponent(TagHelperDescriptor tagHelper) + => tagHelper.DisplayName.StartsWith("Microsoft.AspNetCore.Components.", StringComparison.Ordinal); + + protected static TagHelperDescriptor[] AssertAndExcludeFullyQualifiedNameMatchComponents( + TagHelperDescriptor[] components, + int expectedCount) + { + var fullyQualifiedNameMatchComponents = components.Where(c => c.IsFullyQualifiedNameMatch).ToArray(); + Assert.Equal(expectedCount, fullyQualifiedNameMatchComponents.Length); + + var shortNameMatchComponents = components.Where(c => !c.IsFullyQualifiedNameMatch).ToArray(); + + // For every fully qualified name component, we want to make sure we have a corresponding short name component. + foreach (var fullNameComponent in fullyQualifiedNameMatchComponents) + { + Assert.Contains(shortNameMatchComponents, component => + { + return component.Name == fullNameComponent.Name && + component.Kind == fullNameComponent.Kind && + component.BoundAttributes.SequenceEqual(fullNameComponent.BoundAttributes); + }); + } + + return shortNameMatchComponents; + } + + protected static TagHelperCollection AssertAndExcludeFullyQualifiedNameMatchComponents( + TagHelperCollection collection, + int expectedCount) + { + var fullyQualifiedNameMatchComponents = collection.Where(c => c.IsFullyQualifiedNameMatch); + Assert.Equal(expectedCount, fullyQualifiedNameMatchComponents.Count); + + var shortNameMatchComponents = collection.Where(c => !c.IsFullyQualifiedNameMatch); + + // For every fully qualified name component, we want to make sure we have a corresponding short name component. + foreach (var fullNameComponent in fullyQualifiedNameMatchComponents) + { + Assert.Contains(shortNameMatchComponents, component => + { + return component.Name == fullNameComponent.Name && + component.Kind == fullNameComponent.Kind && + component.BoundAttributes.SequenceEqual(fullNameComponent.BoundAttributes); + }); + } + + return shortNameMatchComponents; + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BindTagHelperProducerTest.cs similarity index 88% rename from src/Compiler/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor/test/BindTagHelperProducerTest.cs index 0d66261992c..b1f39de8005 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BindTagHelperProducerTest.cs @@ -1,20 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System; using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Xunit; namespace Microsoft.CodeAnalysis.Razor; -public class BindTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase +public class BindTagHelperProducerTest : TagHelperDescriptorProviderTestBase { + protected override void ConfigureEngine(RazorProjectEngineBuilder builder) + { + builder.Features.Add(new BindTagHelperProducer.Factory()); + builder.Features.Add(new ComponentTagHelperProducer.Factory()); + } + [Fact] - public void Execute_FindsBindTagHelperOnComponentType_Delegate_CreatesDescriptor() + public void GetTagHelpers_FindsBindTagHelperOnComponentType_Delegate_CreatesTagHelper() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -48,19 +53,11 @@ public Task SetParametersAsync(ParameterView parameters) Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - - // We run after component discovery and depend on the results. - var componentProvider = new ComponentTagHelperDescriptorProvider(); - componentProvider.Execute(context); - - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 1); var bind = Assert.Single(matches); @@ -171,56 +168,7 @@ public Task SetParametersAsync(ParameterView parameters) } [Fact] - public void Execute_BindTagHelperReturnsValuesWhenProvidedNoTargetSymbol() - { - // When BindTagHelperDescriptorProvider is given a compilation that references - // API assemblies with "BindConverter" and "BindAttributes", but no target symbol, - // it will find the expected tag helpers. - - // Arrange - var compilation = BaseCompilation; - - Assert.Empty(compilation.GetDiagnostics()); - - var context = new TagHelperDescriptorProviderContext(compilation); - - var bindTagHelperProvider = new BindTagHelperDescriptorProvider(); - - // Act - bindTagHelperProvider.Execute(context); - - // Assert - var matches = context.Results.Where(static t => t.Kind == TagHelperKind.Bind); - Assert.NotEmpty(matches); - } - - [Fact] - public void Execute_BindTagHelperReturnsValuesWhenProvidedCorrectAssemblyTargetSymbol() - { - // When BindTagHelperDescriptorProvider is given a compilation that references - // API assemblies with "BindConverter", and a target symbol matching the assembly - // containing "BindConverter", it will find the expected tag helpers. - - // Arrange - var compilation = BaseCompilation; - - Assert.Empty(compilation.GetDiagnostics()); - - var bindConverterSymbol = compilation.GetTypeByMetadataName(ComponentsApi.BindConverter.FullTypeName); - var context = new TagHelperDescriptorProviderContext(compilation, bindConverterSymbol.ContainingAssembly); - - var bindTagHelperProvider = new BindTagHelperDescriptorProvider(); - - // Act - bindTagHelperProvider.Execute(context); - - // Assert - var matches = context.Results.Where(static t => t.Kind == TagHelperKind.Bind); - Assert.NotEmpty(matches); - } - - [Fact] - public void Execute_BindTagHelperReturnsEmptyWhenCompilationAssemblyTargetSymbol() + public void GetTagHelpers_BindTagHelperReturnsEmptyWhenCompilationAssemblyTargetSymbol() { // When BindTagHelperDescriptorProvider is given a compilation that references // API assemblies with "BindConverter", and a target symbol that does not match the @@ -231,20 +179,16 @@ public void Execute_BindTagHelperReturnsEmptyWhenCompilationAssemblyTargetSymbol Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation, compilation.Assembly); - - var bindTagHelperProvider = new BindTagHelperDescriptorProvider(); - // Act - bindTagHelperProvider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); Assert.Empty(matches); } [Fact] - public void Execute_FindsBindTagHelperOnComponentType_EventCallback_CreatesDescriptor() + public void GetTagHelpers_FindsBindTagHelperOnComponentType_EventCallback_CreatesTagHelper() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -273,19 +217,11 @@ public Task SetParametersAsync(ParameterView parameters) Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - - // We run after component discovery and depend on the results. - var componentProvider = new ComponentTagHelperDescriptorProvider(); - componentProvider.Execute(context); - - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 1); var bind = Assert.Single(matches); @@ -395,7 +331,7 @@ public Task SetParametersAsync(ParameterView parameters) } [Fact] - public void Execute_NoMatchedPropertiesOnComponent_IgnoresComponent() + public void GetTagHelpers_NoMatchedPropertiesOnComponent_IgnoresComponent() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -423,25 +359,17 @@ public Task SetParametersAsync(ParameterView parameters) Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - - // We run after component discovery and depend on the results. - var componentProvider = new ComponentTagHelperDescriptorProvider(); - componentProvider.Execute(context); - - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); Assert.Empty(matches); } [Fact] - public void Execute_BindOnElement_CreatesDescriptor() + public void GetTagHelpers_BindOnElement_CreatesTagHelper() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -458,14 +386,11 @@ public class BindAttributes Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); @@ -700,7 +625,7 @@ static void AssertAttribute(BoundAttributeDescriptor attribute) } [Fact] - public void Execute_BindOnElementWithSuffix_CreatesDescriptor() + public void GetTagHelpers_BindOnElementWithSuffix_CreatesTagHelper() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -717,14 +642,11 @@ public class BindAttributes Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); @@ -783,7 +705,7 @@ public class BindAttributes } [Fact] - public void Execute_BindOnInputElementWithoutTypeAttribute_CreatesDescriptor() + public void GetTagHelpers_BindOnInputElementWithoutTypeAttribute_CreatesTagHelper() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -800,14 +722,11 @@ public class BindAttributes Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); @@ -857,7 +776,7 @@ public class BindAttributes } [Fact] - public void Execute_BindOnInputElementWithTypeAttribute_CreatesDescriptor() + public void GetTagHelpers_BindOnInputElementWithTypeAttribute_CreatesTagHelper() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -874,14 +793,11 @@ public class BindAttributes Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); @@ -952,7 +868,7 @@ public class BindAttributes } [Fact] - public void Execute_BindOnInputElementWithTypeAttributeAndSuffix_CreatesDescriptor() + public void GetTagHelpers_BindOnInputElementWithTypeAttributeAndSuffix_CreatesTagHelper() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -969,14 +885,11 @@ public class BindAttributes Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); @@ -1049,7 +962,7 @@ public class BindAttributes } [Fact] - public void Execute_BindOnInputElementWithTypeAttributeAndSuffixAndInvariantCultureAndFormat_CreatesDescriptor() + public void GetTagHelpers_BindOnInputElementWithTypeAttributeAndSuffixAndInvariantCultureAndFormat_CreatesTagHelper() { // Arrange var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" @@ -1066,14 +979,11 @@ public class BindAttributes Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetBindTagHelpers(context); + var matches = GetBindTagHelpers(result); matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); @@ -1087,20 +997,17 @@ public class BindAttributes } [Fact] - public void Execute_BindFallback_CreatesDescriptor() + public void GetTagHelpers_BindFallback_CreatesTagHelper() { // Arrange var compilation = BaseCompilation; Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new BindTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var bind = Assert.Single(context.Results, r => r.IsFallbackBindTagHelper()); + var bind = Assert.Single(result, r => r.IsFallbackBindTagHelper()); // These are features Bind Tags Helpers don't use. Verifying them once here and // then ignoring them. @@ -1233,6 +1140,6 @@ public void Execute_BindFallback_CreatesDescriptor() Assert.False(parameter.IsEnum); } - private static TagHelperDescriptor[] GetBindTagHelpers(TagHelperDescriptorProviderContext context) - => [.. ExcludeBuiltInComponents(context).Where(static t => t.Kind == TagHelperKind.Bind)]; + private static TagHelperCollection GetBindTagHelpers(TagHelperCollection collection) + => collection.Where(static t => t.Kind == TagHelperKind.Bind && !IsBuiltInComponent(t)); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/CompilationTagHelperFeatureTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/CompilationTagHelperFeatureTest.cs index 5d9815355aa..ba19e13c1de 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/CompilationTagHelperFeatureTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/CompilationTagHelperFeatureTest.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Linq; using System.Threading; using Microsoft.AspNetCore.Razor.Language; @@ -23,6 +21,7 @@ public void IsValidCompilation_ReturnsTrueIfTagHelperInterfaceCannotBeFound() { ReferenceUtil.NetLatestSystemRuntime, }; + var compilation = CSharpCompilation.Create("Test", references: references); // Act @@ -40,6 +39,7 @@ public void IsValidCompilation_ReturnsFalseIfSystemStringCannotBeFound() { ReferenceUtil.AspNetLatestRazor, }; + var compilation = CSharpCompilation.Create("Test", references: references); // Act @@ -58,6 +58,7 @@ public void IsValidCompilation_ReturnsTrueIfWellKnownTypesAreFound() ReferenceUtil.NetLatestSystemRuntime, ReferenceUtil.AspNetLatestRazor, }; + var compilation = CSharpCompilation.Create("Test", references: references); // Act @@ -68,23 +69,27 @@ public void IsValidCompilation_ReturnsTrueIfWellKnownTypesAreFound() } [Fact] - public void GetDescriptors_DoesNotSetCompilation_IfCompilationIsInvalid() + public void GetTagHelpers_DoesNotSetCompilation_IfCompilationIsInvalid() { // Arrange - var provider = new Mock(); - provider.Setup(c => c.Execute(It.IsAny(), It.IsAny())); + var serviceMock = new Mock(); + serviceMock + .Setup(service => service.GetTagHelpers(It.IsAny(), It.IsAny())) + .Returns(TagHelperCollection.Empty); var engine = RazorProjectEngine.Create( - configure => + builder => { - configure.ConfigureParserOptions(builder => + builder.ConfigureParserOptions(static builder => { builder.UseRoslynTokenizer = true; }); - configure.Features.Add(new DefaultMetadataReferenceFeature()); - configure.Features.Add(provider.Object); - configure.Features.Add(new CompilationTagHelperFeature()); + builder.Features.Add(new DefaultMetadataReferenceFeature()); + builder.Features.Add(new CompilationTagHelperFeature()); + + var oldFeature = builder.Features.OfType().Single(); + builder.Features.Replace(oldFeature, serviceMock.Object); }); var feature = engine.Engine.GetFeatures().First(); @@ -94,18 +99,19 @@ public void GetDescriptors_DoesNotSetCompilation_IfCompilationIsInvalid() // Assert Assert.Empty(result); - provider.Verify(c => c.Execute(It.IsAny(), It.IsAny()), Times.Never); + serviceMock.Verify(c => c.GetTagHelpers(It.IsAny(), It.IsAny()), Times.Never); } [Fact] - public void GetDescriptors_SetsCompilation_IfCompilationIsValid() + public void GetTagHelpers_SetsCompilation_IfCompilationIsValid() { // Arrange - Compilation compilation = null; - var provider = new Mock(); - provider - .Setup(c => c.Execute(It.IsAny(), It.IsAny())) - .Callback((TagHelperDescriptorProviderContext c, CancellationToken ct) => compilation = c.Compilation) + Compilation? compilation = null; + var serviceMock = new Mock(); + serviceMock + .Setup(service => service.GetTagHelpers(It.IsAny(), It.IsAny())) + .Callback((Compilation c, CancellationToken ct) => compilation = c) + .Returns(TagHelperCollection.Empty) .Verifiable(); var references = new[] @@ -115,16 +121,18 @@ public void GetDescriptors_SetsCompilation_IfCompilationIsValid() }; var engine = RazorProjectEngine.Create( - configure => + builder => { - configure.ConfigureParserOptions(builder => + builder.ConfigureParserOptions(static builder => { builder.UseRoslynTokenizer = true; }); - configure.Features.Add(new DefaultMetadataReferenceFeature { References = references }); - configure.Features.Add(provider.Object); - configure.Features.Add(new CompilationTagHelperFeature()); + builder.Features.Add(new DefaultMetadataReferenceFeature { References = references }); + builder.Features.Add(new CompilationTagHelperFeature()); + + var oldFeature = builder.Features.OfType().Single(); + builder.Features.Replace(oldFeature, serviceMock.Object); }); var feature = engine.Engine.GetFeatures().First(); @@ -134,7 +142,7 @@ public void GetDescriptors_SetsCompilation_IfCompilationIsValid() // Assert Assert.Empty(result); - provider.Verify(); + serviceMock.Verify(); Assert.NotNull(compilation); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperProducerTest.cs similarity index 87% rename from src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperProducerTest.cs index 88038cefa21..9afe4a064d3 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperProducerTest.cs @@ -1,17 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Xunit; namespace Microsoft.CodeAnalysis.Razor; -public class ComponentTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase +public class ComponentTagHelperProducerTest : TagHelperDescriptorProviderTestBase { + protected override void ConfigureEngine(RazorProjectEngineBuilder builder) + { + builder.Features.Add(new BindTagHelperProducer.Factory()); + builder.Features.Add(new ComponentTagHelperProducer.Factory()); + } + [Fact] public void Execute_FindsIComponentType_CreatesDescriptor() { @@ -41,14 +46,11 @@ public Task SetParametersAsync(ParameterView parameters) Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -152,14 +154,11 @@ public Task SetParametersAsync(ParameterView parameters) Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -214,14 +213,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -254,14 +250,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -292,14 +285,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -330,14 +320,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -381,14 +368,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -427,14 +411,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -473,14 +454,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -535,14 +513,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -608,14 +583,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -656,14 +628,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -716,14 +685,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -765,14 +731,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -818,14 +781,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -879,14 +839,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -934,14 +891,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -1007,14 +961,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -1077,14 +1028,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -1157,14 +1105,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -1237,14 +1182,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -1321,14 +1263,11 @@ public class Context Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -1405,14 +1344,11 @@ public class MyComponent : ComponentBase Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 4); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -1495,14 +1431,11 @@ public string this[int i] Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); @@ -1549,14 +1482,11 @@ public class MyDerivedComponent2 : MyDerivedComponent1 Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var components = ExcludeBuiltInComponents(context); + var components = result.Where(c => !IsBuiltInComponent(c)); components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components, c => c.Kind == TagHelperKind.Component); @@ -1607,20 +1537,19 @@ public Task SetParametersAsync(ParameterView parameters) Assert.Empty(compilation.GetDiagnostics()); - var targetAssembly = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol( - compilation.References.First(static r => r.Display.Contains("Microsoft.CodeAnalysis.Razor.Test"))); - - var context = new TagHelperDescriptorProviderContext(compilation, targetAssembly); - var provider = new ComponentTagHelperDescriptorProvider(); + var targetAssembly = (IAssemblySymbol?)compilation.GetAssemblyOrModuleSymbol( + compilation.References.First(static r => r.Display?.Contains("Microsoft.CodeAnalysis.Razor.Test") == true)); + Assert.NotNull(targetAssembly); // Act - provider.Execute(context); + Assert.True(TryGetDiscoverer(compilation, out var discoverer)); + var result = discoverer.GetTagHelpers(targetAssembly); // Assert Assert.NotNull(compilation.GetTypeByMetadataName(testComponent)); - Assert.Empty(context.Results); // Target assembly contains no components - Assert.Empty(context.Results.Where(f => f.TypeName == testComponent)); - Assert.Empty(context.Results.Where(f => f.TypeName == routerComponent)); + Assert.Empty(result); // Target assembly contains no components + Assert.Empty(result.Where(f => f.TypeName == testComponent)); + Assert.Empty(result.Where(f => f.TypeName == routerComponent)); } [Fact] @@ -1654,16 +1583,13 @@ public Task SetParametersAsync(ParameterView parameters) Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new ComponentTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert Assert.NotNull(compilation.GetTypeByMetadataName(testComponent)); - Assert.NotEmpty(context.Results); - Assert.NotEmpty(context.Results.Where(f => f.TypeName == testComponent)); - Assert.NotEmpty(context.Results.Where(f => f.TypeName == routerComponent)); + Assert.NotEmpty(result); + Assert.NotEmpty(result.Where(f => f.TypeName == testComponent)); + Assert.NotEmpty(result.Where(f => f.TypeName == routerComponent)); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperProducerTest.cs similarity index 55% rename from src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperProducerTest.cs index 39e8febc96d..be7f3796f02 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperProducerTest.cs @@ -1,37 +1,33 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Linq; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Xunit; namespace Microsoft.CodeAnalysis.Razor; -public class DefaultTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase +public class DefaultTagHelperProducerTest : TagHelperDescriptorProviderTestBase { + protected override void ConfigureEngine(RazorProjectEngineBuilder builder) + { + builder.Features.Add(new DefaultTagHelperProducer.Factory()); + } + [Fact] public void Execute_DoesNotAddEditorBrowsableNeverDescriptorsAtDesignTime() { // Arrange var editorBrowsableTypeName = "TestNamespace.EditorBrowsableTagHelper"; var compilation = BaseCompilation; - var descriptorProvider = new DefaultTagHelperDescriptorProvider(); - - var context = new TagHelperDescriptorProviderContext(compilation) - { - ExcludeHidden = true - }; // Act - descriptorProvider.Execute(context); + var result = GetTagHelpers(compilation, TagHelperDiscoveryOptions.ExcludeHidden); // Assert Assert.NotNull(compilation.GetTypeByMetadataName(editorBrowsableTypeName)); - var nullDescriptors = context.Results.Where(descriptor => descriptor == null); - Assert.Empty(nullDescriptors); - var editorBrowsableDescriptor = context.Results.Where(descriptor => descriptor.TypeName == editorBrowsableTypeName); + var editorBrowsableDescriptor = result.Where(descriptor => descriptor.TypeName == editorBrowsableTypeName); Assert.Empty(editorBrowsableDescriptor); } @@ -51,18 +47,15 @@ public override void Process(TagHelperContext context, TagHelperOutput output) { } }"; var compilation = BaseCompilation.AddSyntaxTrees(Parse(csharp)); - var descriptorProvider = new DefaultTagHelperDescriptorProvider(); - - var context = new TagHelperDescriptorProviderContext(compilation); // Act - descriptorProvider.Execute(context); + var result = GetTagHelpers(compilation); // Assert Assert.NotNull(compilation.GetTypeByMetadataName(testTagHelper)); - Assert.NotEmpty(context.Results); - Assert.NotEmpty(context.Results.Where(f => f.TypeName == testTagHelper)); - Assert.NotEmpty(context.Results.Where(f => f.TypeName == enumTagHelper)); + Assert.NotEmpty(result); + Assert.NotEmpty(result.Where(f => f.TypeName == testTagHelper)); + Assert.NotEmpty(result.Where(f => f.TypeName == enumTagHelper)); } [Fact] @@ -81,20 +74,21 @@ public override void Process(TagHelperContext context, TagHelperOutput output) { } }"; var compilation = BaseCompilation.AddSyntaxTrees(Parse(csharp)); - var descriptorProvider = new DefaultTagHelperDescriptorProvider(); - var targetAssembly = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol( - compilation.References.First(static r => r.Display.Contains("Microsoft.CodeAnalysis.Razor.Test"))); + var targetAssembly = (IAssemblySymbol?)compilation.GetAssemblyOrModuleSymbol( + compilation.References.First(static r => r.Display?.Contains("Microsoft.CodeAnalysis.Razor.Test") == true)); + + Assert.NotNull(targetAssembly); - var context = new TagHelperDescriptorProviderContext(compilation, targetAssembly); + Assert.True(TryGetDiscoverer(compilation, out var discoverer)); // Act - descriptorProvider.Execute(context); + var result = discoverer.GetTagHelpers(targetAssembly); // Assert Assert.NotNull(compilation.GetTypeByMetadataName(testTagHelper)); - Assert.NotEmpty(context.Results); - Assert.Empty(context.Results.Where(f => f.TypeName == testTagHelper)); - Assert.NotEmpty(context.Results.Where(f => f.TypeName == enumTagHelper)); + Assert.NotEmpty(result); + Assert.Empty(result.Where(f => f.TypeName == testTagHelper)); + Assert.NotEmpty(result.Where(f => f.TypeName == enumTagHelper)); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperProducerTest.cs similarity index 89% rename from src/Compiler/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperProducerTest.cs index d615ac0c469..2d6223d3ab0 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperProducerTest.cs @@ -1,16 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Xunit; namespace Microsoft.CodeAnalysis.Razor; -public class EventHandlerTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase +public class EventHandlerTagHelperProducerTest : TagHelperDescriptorProviderTestBase { + protected override void ConfigureEngine(RazorProjectEngineBuilder builder) + { + builder.Features.Add(new EventHandlerTagHelperProducer.Factory()); + } + [Fact] public void Execute_EventHandler_TwoArgumentsCreatesDescriptor() { @@ -31,14 +35,11 @@ public class EventHandlers Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new EventHandlerTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetEventHandlerTagHelpers(context); + var matches = GetEventHandlerTagHelpers(result); var item = Assert.Single(matches); // These are features Event Handler Tag Helpers don't use. Verifying them once here and @@ -137,14 +138,11 @@ public class EventHandlers Assert.Empty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new EventHandlerTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetEventHandlerTagHelpers(context); + var matches = GetEventHandlerTagHelpers(result); var item = Assert.Single(matches); // These are features Event Handler Tag Helpers don't use. Verifying them once here and @@ -274,14 +272,11 @@ public class EventHandlers Assert.NotEmpty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new EventHandlerTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetEventHandlerTagHelpers(context); + var matches = GetEventHandlerTagHelpers(result); Assert.Empty(matches); } @@ -305,14 +300,11 @@ public class EventHandlers Assert.NotEmpty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new EventHandlerTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetEventHandlerTagHelpers(context); + var matches = GetEventHandlerTagHelpers(result); Assert.Empty(matches); } @@ -336,17 +328,14 @@ public class EventHandlers Assert.NotEmpty(compilation.GetDiagnostics()); - var context = new TagHelperDescriptorProviderContext(compilation); - var provider = new EventHandlerTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(compilation); // Assert - var matches = GetEventHandlerTagHelpers(context); + var matches = GetEventHandlerTagHelpers(result); Assert.Empty(matches); } - private static TagHelperDescriptor[] GetEventHandlerTagHelpers(TagHelperDescriptorProviderContext context) - => [.. ExcludeBuiltInComponents(context).Where(static t => t.Kind == TagHelperKind.EventHandler)]; + private static TagHelperCollection GetEventHandlerTagHelpers(TagHelperCollection collection) + => collection.Where(static t => t.Kind == TagHelperKind.EventHandler && !IsBuiltInComponent(t)); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/KeyTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/KeyTagHelperProducerTest.cs similarity index 87% rename from src/Compiler/Microsoft.CodeAnalysis.Razor/test/KeyTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor/test/KeyTagHelperProducerTest.cs index ff76acc9162..1602324a796 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/KeyTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/KeyTagHelperProducerTest.cs @@ -1,27 +1,28 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Xunit; namespace Microsoft.CodeAnalysis.Razor; -public class KeyTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase +public class KeyTagHelperProducerTest : TagHelperDescriptorProviderTestBase { - [Fact] - public void Execute_CreatesDescriptor() + protected override void ConfigureEngine(RazorProjectEngineBuilder builder) { - // Arrange - var context = new TagHelperDescriptorProviderContext(BaseCompilation); - var provider = new KeyTagHelperDescriptorProvider(); + builder.Features.Add(new KeyTagHelperProducer.Factory()); + } + [Fact] + public void GetTagHelpers_CreatesTagHelper() + { // Act - provider.Execute(context); + var result = GetTagHelpers(BaseCompilation); // Assert - var matches = context.Results.Where(static result => result.Kind == TagHelperKind.Key); + var matches = result.Where(static result => result.Kind == TagHelperKind.Key); var item = Assert.Single(matches); Assert.Empty(item.AllowedChildTags); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/RefTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/RefTagHelperProducerTest.cs similarity index 87% rename from src/Compiler/Microsoft.CodeAnalysis.Razor/test/RefTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor/test/RefTagHelperProducerTest.cs index 22ba76006e0..f950c3f1459 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/RefTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/RefTagHelperProducerTest.cs @@ -1,27 +1,28 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Xunit; namespace Microsoft.CodeAnalysis.Razor; -public class RefTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase +public class RefTagHelperProducerTest : TagHelperDescriptorProviderTestBase { - [Fact] - public void Execute_CreatesDescriptor() + protected override void ConfigureEngine(RazorProjectEngineBuilder builder) { - // Arrange - var context = new TagHelperDescriptorProviderContext(BaseCompilation); - var provider = new RefTagHelperDescriptorProvider(); + builder.Features.Add(new RefTagHelperProducer.Factory()); + } + [Fact] + public void GetTagHelpers_CreatesTagHelper() + { // Act - provider.Execute(context); + var result = GetTagHelpers(BaseCompilation); // Assert - var matches = context.Results.Where(static result => result.Kind == TagHelperKind.Ref); + var matches = result.Where(static result => result.Kind == TagHelperKind.Ref); var item = Assert.Single(matches); Assert.Empty(item.AllowedChildTags); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/SplatTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/SplatTagHelperProducerTest.cs similarity index 87% rename from src/Compiler/Microsoft.CodeAnalysis.Razor/test/SplatTagHelperDescriptorProviderTest.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor/test/SplatTagHelperProducerTest.cs index f5504025dea..237f03b3e28 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/SplatTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/SplatTagHelperProducerTest.cs @@ -1,26 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Linq; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Xunit; namespace Microsoft.CodeAnalysis.Razor; -public class SplatTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase +public class SplatTagHelperProducerTest : TagHelperDescriptorProviderTestBase { + protected override void ConfigureEngine(RazorProjectEngineBuilder builder) + { + builder.Features.Add(new SplatTagHelperProducer.Factory()); + } + [Fact] public void Execute_CreatesDescriptor() { - // Arrange - var context = new TagHelperDescriptorProviderContext(BaseCompilation); - var provider = new SplatTagHelperDescriptorProvider(); - // Act - provider.Execute(context); + var result = GetTagHelpers(BaseCompilation); // Assert - var matches = context.Results.Where(static result => result.Kind == TagHelperKind.Splat); + var matches = result.Where(static result => result.Kind == TagHelperKind.Splat); var item = Assert.Single(matches); Assert.Empty(item.AllowedChildTags); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs index 673ecf4f253..614056c8751 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Immutable; +using System.Diagnostics; using Microsoft.AspNetCore.Razor; using Microsoft.CodeAnalysis.Razor.Tooltip; namespace Microsoft.CodeAnalysis.Razor.Completion; +[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] internal sealed class RazorCompletionItem { public RazorCompletionItemKind Kind { get; } @@ -24,6 +26,9 @@ internal sealed class RazorCompletionItem public bool IsSnippet { get; } public TextEdit[]? AdditionalTextEdits { get; } + private string GetDebuggerDisplay() + => $"{Kind}: {DisplayText}"; + /// /// Creates a new Razor completion item /// diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs index d7319c698d5..13191609a95 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Buffers; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -10,36 +9,25 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Threading; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.Telemetry; using Microsoft.NET.Sdk.Razor.SourceGenerators; namespace Microsoft.CodeAnalysis; internal static class ProjectExtensions { - private const string GetTagHelpersEventName = "taghelperresolver/gettaghelpers"; - private const string PropertySuffix = ".elapsedtimems"; - /// /// Gets the available tag helpers from the specified /// using the given . /// - /// - /// A telemetry event will be reported to . - /// public static async ValueTask GetTagHelpersAsync( this Project project, RazorProjectEngine projectEngine, - ITelemetryReporter telemetryReporter, CancellationToken cancellationToken) { - var providers = GetTagHelperDescriptorProviders(projectEngine); - - if (providers is []) + if (!projectEngine.Engine.TryGetFeature(out ITagHelperDiscoveryService? discoveryService)) { return []; } @@ -50,36 +38,12 @@ public static async ValueTask GetTagHelpersAsync( return []; } - using var builder = new TagHelperCollection.Builder(); - using var pooledWatch = StopwatchPool.GetPooledObject(out var watch); - using var pooledSpan = ArrayPool.Shared.GetPooledArraySpan(minimumLength: providers.Length, out var properties); - - var context = new TagHelperDescriptorProviderContext(compilation, builder) - { - ExcludeHidden = true, - IncludeDocumentation = true - }; - - var writeProperties = properties; + const TagHelperDiscoveryOptions Options = TagHelperDiscoveryOptions.ExcludeHidden | + TagHelperDiscoveryOptions.IncludeDocumentation; - foreach (var provider in providers) - { - watch.Restart(); - provider.Execute(context, cancellationToken); - watch.Stop(); - - writeProperties[0] = new(provider.GetType().Name + PropertySuffix, watch.ElapsedMilliseconds); - writeProperties = writeProperties[1..]; - } - - telemetryReporter.ReportEvent(GetTagHelpersEventName, Severity.Normal, properties); - - return builder.ToCollection(); + return discoveryService.GetTagHelpers(compilation, Options, cancellationToken); } - private static ImmutableArray GetTagHelperDescriptorProviders(RazorProjectEngine projectEngine) - => projectEngine.Engine.GetFeatures().OrderByAsArray(static x => x.Order); - public static Task TryGetCSharpDocumentFromGeneratedDocumentUriAsync(this Project project, Uri generatedDocumentUri, CancellationToken cancellationToken) { if (!TryGetHintNameFromGeneratedDocumentUri(project, generatedDocumentUri, out var hintName)) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Utilities/RazorProjectInfoFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Utilities/RazorProjectInfoFactory.cs index 22a25543a6d..5c60d8b27fd 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Utilities/RazorProjectInfoFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Utilities/RazorProjectInfoFactory.cs @@ -16,7 +16,6 @@ using Microsoft.CodeAnalysis.Razor.ProjectEngineHost; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Serialization; -using Microsoft.CodeAnalysis.Razor.Telemetry; namespace Microsoft.CodeAnalysis.Razor.Utilities; @@ -88,7 +87,7 @@ public static async Task ConvertAsync(Project project, Cancell fileSystem, configure: defaultConfigure); - var tagHelpers = await project.GetTagHelpersAsync(engine, NoOpTelemetryReporter.Instance, cancellationToken).ConfigureAwait(false); + var tagHelpers = await project.GetTagHelpersAsync(engine, cancellationToken).ConfigureAwait(false); var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/TagHelpers/RemoteTagHelperResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/TagHelpers/RemoteTagHelperResolver.cs index e3fdcd34971..4a361c50a4f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/TagHelpers/RemoteTagHelperResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/TagHelpers/RemoteTagHelperResolver.cs @@ -8,21 +8,17 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.ProjectEngineHost; -using Microsoft.CodeAnalysis.Razor.Telemetry; namespace Microsoft.CodeAnalysis.Remote.Razor; [Export(typeof(RemoteTagHelperResolver)), Shared] -[method: ImportingConstructor] -internal class RemoteTagHelperResolver(ITelemetryReporter telemetryReporter) +internal class RemoteTagHelperResolver { /// /// A map of configuration names to instances. /// private static readonly Dictionary s_configurationNameToFactoryMap = CreateConfigurationNameToFactoryMap(); - private readonly ITelemetryReporter _telemetryReporter = telemetryReporter; - private static Dictionary CreateConfigurationNameToFactoryMap() { var map = new Dictionary(StringComparer.Ordinal); @@ -40,7 +36,7 @@ public ValueTask GetTagHelpersAsync( RazorConfiguration? configuration, CancellationToken cancellationToken) => configuration is not null - ? workspaceProject.GetTagHelpersAsync(CreateProjectEngine(configuration), _telemetryReporter, cancellationToken) + ? workspaceProject.GetTagHelpersAsync(CreateProjectEngine(configuration), cancellationToken) : new([]); private static RazorProjectEngine CreateProjectEngine(RazorConfiguration configuration) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Discovery/OutOfProcTagHelperResolver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Discovery/OutOfProcTagHelperResolver.cs index 2e890e6fdda..cd084033aaa 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Discovery/OutOfProcTagHelperResolver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Discovery/OutOfProcTagHelperResolver.cs @@ -234,5 +234,5 @@ protected virtual ValueTask ResolveTagHelpersInProcessAsync Project project, ProjectSnapshot projectSnapshot, CancellationToken cancellationToken) - => project.GetTagHelpersAsync(projectSnapshot.ProjectEngine, _telemetryReporter, cancellationToken); + => project.GetTagHelpersAsync(projectSnapshot.ProjectEngine, cancellationToken); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs index c0a706c7b37..5b45402332f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs @@ -391,7 +391,7 @@ private static async Task GetStandardTagHelpersAsync(Cancel fileSystem, configure: null); - var tagHelpers = await project.GetTagHelpersAsync(engine, NoOpTelemetryReporter.Instance, cancellationToken).ConfigureAwait(false); + var tagHelpers = await project.GetTagHelpersAsync(engine, cancellationToken).ConfigureAwait(false); Assert.NotEmpty(tagHelpers); return tagHelpers; diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectEngineHost/ProjectEngineFactoryProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectEngineHost/ProjectEngineFactoryProviderTest.cs index 5e73178795e..668f1c0dc34 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectEngineHost/ProjectEngineFactoryProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectEngineHost/ProjectEngineFactoryProviderTest.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.IO; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Xunit; @@ -82,7 +83,7 @@ public void Create_CreatesDesignTimeTemplateEngine_ForVersion3_0() // Assert Assert.Single(engine.Engine.GetFeatures()); - Assert.Single(engine.Engine.GetFeatures()); + Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); } @@ -106,7 +107,7 @@ public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_1() Assert.Single(engine.Engine.GetFeatures()); Assert.Empty(engine.Engine.GetFeatures()); - Assert.Single(engine.Engine.GetFeatures()); + Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); } @@ -128,7 +129,7 @@ public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_0() // Assert Assert.Single(engine.Engine.GetFeatures()); - Assert.Single(engine.Engine.GetFeatures()); + Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); } @@ -150,7 +151,7 @@ public void Create_CreatesTemplateEngine_ForVersion1_1() // Assert Assert.Single(engine.Engine.GetFeatures()); - Assert.Single(engine.Engine.GetFeatures()); + Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); } @@ -174,15 +175,15 @@ public void Create_DoesNotSupportViewComponentTagHelpers_ForVersion1_0() Assert.Single(engine.Engine.GetFeatures()); Assert.Single(engine.Engine.GetFeatures()); - Assert.Empty(engine.Engine.GetFeatures()); + Assert.Empty(engine.Engine.GetFeatures()); Assert.Empty(engine.Engine.GetFeatures()); Assert.Empty(engine.Engine.GetFeatures()); - Assert.Empty(engine.Engine.GetFeatures()); + Assert.Empty(engine.Engine.GetFeatures()); Assert.Empty(engine.Engine.GetFeatures()); Assert.Empty(engine.Engine.GetFeatures()); - Assert.Empty(engine.Engine.GetFeatures()); + Assert.Empty(engine.Engine.GetFeatures()); Assert.Empty(engine.Engine.GetFeatures()); } @@ -202,8 +203,8 @@ public void Create_ForUnknownConfiguration_UsesFallbackFactory() // Assert Assert.Single(engine.Engine.GetFeatures()); - Assert.Empty(engine.Engine.GetFeatures()); - Assert.Empty(engine.Engine.GetFeatures()); + Assert.Empty(engine.Engine.GetFeatures()); + Assert.Empty(engine.Engine.GetFeatures()); Assert.Empty(engine.Engine.GetFeatures()); Assert.Empty(engine.Engine.GetFeatures()); }