Add support for `ViewComponentTagHelpers`.
- Hardcoded `ViewComponent` discovery. - Hardcoded `ViewComponentTagHelperDescriptor` creation. - Added test to validate that ViewComponents are discovered and transitioned into TagHelpers properly. - Avoided adding a reference to MVC to prevent circular references. This resulted in custom marker attributes to represent `ViewComponent`s. Also made a lot of use of `ViewComponent` conventions (ending in "ViewComponent"). #932
This commit is contained in:
parent
a84f35022e
commit
6e647854fa
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Razor.Evolution;
|
using Microsoft.AspNetCore.Razor.Evolution;
|
||||||
using Microsoft.AspNetCore.Razor.Evolution.Legacy;
|
using Microsoft.AspNetCore.Razor.Evolution.Legacy;
|
||||||
|
|
||||||
|
|
@ -9,9 +11,18 @@ namespace Microsoft.CodeAnalysis.Razor
|
||||||
{
|
{
|
||||||
internal class DefaultTagHelperResolver : TagHelperResolver
|
internal class DefaultTagHelperResolver : TagHelperResolver
|
||||||
{
|
{
|
||||||
public DefaultTagHelperResolver(bool designTime)
|
private static readonly Version SupportedVCTHMvcVersion = new Version(1, 1);
|
||||||
|
private readonly string ViewComponentAssembly;
|
||||||
|
|
||||||
|
public DefaultTagHelperResolver(bool designTime) : this(designTime, ViewComponentTypes.Assembly)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal for testing
|
||||||
|
internal DefaultTagHelperResolver(bool designTime, string viewComponentAssembly)
|
||||||
{
|
{
|
||||||
DesignTime = designTime;
|
DesignTime = designTime;
|
||||||
|
ViewComponentAssembly = viewComponentAssembly;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool DesignTime { get; }
|
public bool DesignTime { get; }
|
||||||
|
|
@ -19,17 +30,75 @@ namespace Microsoft.CodeAnalysis.Razor
|
||||||
public override IReadOnlyList<TagHelperDescriptor> GetTagHelpers(Compilation compilation)
|
public override IReadOnlyList<TagHelperDescriptor> GetTagHelpers(Compilation compilation)
|
||||||
{
|
{
|
||||||
var results = new List<TagHelperDescriptor>();
|
var results = new List<TagHelperDescriptor>();
|
||||||
|
var errors = new ErrorSink();
|
||||||
|
|
||||||
// If ITagHelper isn't defined, then we couldn't possibly find anything.
|
VisitTagHelpers(compilation, results, errors);
|
||||||
|
VisitViewComponents(compilation, results, errors);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VisitTagHelpers(Compilation compilation, List<TagHelperDescriptor> results, ErrorSink errors)
|
||||||
|
{
|
||||||
var @interface = compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper);
|
var @interface = compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper);
|
||||||
if (@interface == null)
|
if (@interface == null)
|
||||||
{
|
{
|
||||||
return results;
|
// If ITagHelper isn't defined, then we couldn't possibly find anything.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var types = new List<INamedTypeSymbol>();
|
var types = new List<INamedTypeSymbol>();
|
||||||
var visitor = new Visitor(@interface, types);
|
var visitor = new TagHelperVisitor(@interface, types);
|
||||||
|
|
||||||
|
VisitCompilation(visitor, compilation);
|
||||||
|
|
||||||
|
var factory = new DefaultTagHelperDescriptorFactory(compilation, DesignTime);
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
var descriptors = factory.CreateDescriptors(type, errors);
|
||||||
|
results.AddRange(descriptors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VisitViewComponents(Compilation compilation, List<TagHelperDescriptor> results, ErrorSink errors)
|
||||||
|
{
|
||||||
|
var mvcViewFeaturesAssembly = compilation.References
|
||||||
|
.Select(reference => compilation.GetAssemblyOrModuleSymbol(reference))
|
||||||
|
.OfType<IAssemblySymbol>()
|
||||||
|
.FirstOrDefault(assembly => string.Equals(assembly.Identity.Name, ViewComponentAssembly, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
if (mvcViewFeaturesAssembly == null || mvcViewFeaturesAssembly.Identity.Version < SupportedVCTHMvcVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewComponentAttributeSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute);
|
||||||
|
var nonViewComponentAttributeSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute);
|
||||||
|
var types = new List<INamedTypeSymbol>();
|
||||||
|
var visitor = new ViewComponentVisitor(viewComponentAttributeSymbol, viewComponentAttributeSymbol, types);
|
||||||
|
|
||||||
|
VisitCompilation(visitor, compilation);
|
||||||
|
|
||||||
|
var factory = new ViewComponentTagHelperDescriptorFactory(compilation);
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var descriptor = factory.CreateDescriptor(type);
|
||||||
|
|
||||||
|
results.Add(descriptor);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.OnError(SourceLocation.Zero, ex.Message, length: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VisitCompilation(SymbolVisitor visitor, Compilation compilation)
|
||||||
|
{
|
||||||
visitor.Visit(compilation.Assembly.GlobalNamespace);
|
visitor.Visit(compilation.Assembly.GlobalNamespace);
|
||||||
|
|
||||||
foreach (var reference in compilation.References)
|
foreach (var reference in compilation.References)
|
||||||
|
|
@ -39,26 +108,15 @@ namespace Microsoft.CodeAnalysis.Razor
|
||||||
visitor.Visit(assembly.GlobalNamespace);
|
visitor.Visit(assembly.GlobalNamespace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errors = new ErrorSink();
|
|
||||||
var factory = new DefaultTagHelperDescriptorFactory(compilation, DesignTime);
|
|
||||||
|
|
||||||
foreach (var type in types)
|
|
||||||
{
|
|
||||||
var descriptors = factory.CreateDescriptors(type, errors);
|
|
||||||
results.AddRange(descriptors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visits top-level types and finds interface implementations.
|
// Visits top-level types and finds interface implementations.
|
||||||
internal class Visitor : SymbolVisitor
|
internal class TagHelperVisitor : SymbolVisitor
|
||||||
{
|
{
|
||||||
private INamedTypeSymbol _interface;
|
private INamedTypeSymbol _interface;
|
||||||
private List<INamedTypeSymbol> _results;
|
private List<INamedTypeSymbol> _results;
|
||||||
|
|
||||||
public Visitor(INamedTypeSymbol @interface, List<INamedTypeSymbol> results)
|
public TagHelperVisitor(INamedTypeSymbol @interface, List<INamedTypeSymbol> results)
|
||||||
{
|
{
|
||||||
_interface = @interface;
|
_interface = @interface;
|
||||||
_results = results;
|
_results = results;
|
||||||
|
|
@ -88,5 +146,79 @@ namespace Microsoft.CodeAnalysis.Razor
|
||||||
symbol.AllInterfaces.Contains(_interface);
|
symbol.AllInterfaces.Contains(_interface);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class ViewComponentVisitor : SymbolVisitor
|
||||||
|
{
|
||||||
|
private INamedTypeSymbol _viewComponentAttribute;
|
||||||
|
private INamedTypeSymbol _nonViewComponentAttribute;
|
||||||
|
private List<INamedTypeSymbol> _results;
|
||||||
|
|
||||||
|
public ViewComponentVisitor(
|
||||||
|
INamedTypeSymbol viewComponentAttribute,
|
||||||
|
INamedTypeSymbol nonViewComponentAttribute,
|
||||||
|
List<INamedTypeSymbol> results)
|
||||||
|
{
|
||||||
|
_viewComponentAttribute = viewComponentAttribute;
|
||||||
|
_nonViewComponentAttribute = nonViewComponentAttribute;
|
||||||
|
_results = results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VisitNamedType(INamedTypeSymbol symbol)
|
||||||
|
{
|
||||||
|
if (IsViewComponent(symbol))
|
||||||
|
{
|
||||||
|
_results.Add(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (symbol.DeclaredAccessibility != Accessibility.Public)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var member in symbol.GetTypeMembers())
|
||||||
|
{
|
||||||
|
Visit(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VisitNamespace(INamespaceSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach (var member in symbol.GetMembers())
|
||||||
|
{
|
||||||
|
Visit(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsViewComponent(INamedTypeSymbol symbol)
|
||||||
|
{
|
||||||
|
if (symbol.DeclaredAccessibility != Accessibility.Public ||
|
||||||
|
symbol.IsAbstract ||
|
||||||
|
symbol.IsGenericType ||
|
||||||
|
AttributeIsDefined(symbol, _nonViewComponentAttribute))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return symbol.Name.EndsWith(ViewComponentTypes.ViewComponentSuffix) ||
|
||||||
|
AttributeIsDefined(symbol, _viewComponentAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AttributeIsDefined(INamedTypeSymbol type, INamedTypeSymbol queryAttribute)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = type.GetAttributes().Where(a => a.AttributeClass == queryAttribute).FirstOrDefault();
|
||||||
|
|
||||||
|
if (attribute != null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AttributeIsDefined(type.BaseType, queryAttribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
110
src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/ViewComponentResources.Designer.cs
generated
Normal file
110
src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/ViewComponentResources.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// <auto-generated />
|
||||||
|
namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
|
{
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Resources;
|
||||||
|
|
||||||
|
internal static class ViewComponentResources
|
||||||
|
{
|
||||||
|
private static readonly ResourceManager _resourceManager
|
||||||
|
= new ResourceManager("Microsoft.CodeAnalysis.Razor.Workspaces.ViewComponentResources", typeof(ViewComponentResources).GetTypeInfo().Assembly);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// View component '{0}' must have exactly one public method named '{1}' or '{2}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ViewComponent_AmbiguousMethods
|
||||||
|
{
|
||||||
|
get { return GetString("ViewComponent_AmbiguousMethods"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// View component '{0}' must have exactly one public method named '{1}' or '{2}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatViewComponent_AmbiguousMethods(object p0, object p1, object p2)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ViewComponent_AsyncMethod_ShouldReturnTask
|
||||||
|
{
|
||||||
|
get { return GetString("ViewComponent_AsyncMethod_ShouldReturnTask"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatViewComponent_AsyncMethod_ShouldReturnTask(object p0, object p1, object p2)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AsyncMethod_ShouldReturnTask"), p0, p1, p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find an '{0}' or '{1}' method for the view component '{2}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ViewComponent_CannotFindMethod
|
||||||
|
{
|
||||||
|
get { return GetString("ViewComponent_CannotFindMethod"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find an '{0}' or '{1}' method for the view component '{2}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatViewComponent_CannotFindMethod(object p0, object p1, object p2)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod"), p0, p1, p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method '{0}' of view component '{1}' cannot return a {2}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ViewComponent_SyncMethod_CannotReturnTask
|
||||||
|
{
|
||||||
|
get { return GetString("ViewComponent_SyncMethod_CannotReturnTask"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method '{0}' of view component '{1}' cannot return a {2}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatViewComponent_SyncMethod_CannotReturnTask(object p0, object p1, object p2)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_CannotReturnTask"), p0, p1, p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method '{0}' of view component '{1}' should be declared to return a value.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ViewComponent_SyncMethod_ShouldReturnValue
|
||||||
|
{
|
||||||
|
get { return GetString("ViewComponent_SyncMethod_ShouldReturnValue"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method '{0}' of view component '{1}' should be declared to return a value.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatViewComponent_SyncMethod_ShouldReturnValue(object p0, object p1)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_ShouldReturnValue"), p0, p1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
|
{
|
||||||
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.Assert(value != null);
|
||||||
|
|
||||||
|
if (formatterNames != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < formatterNames.Length; i++)
|
||||||
|
{
|
||||||
|
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="ViewComponent_AmbiguousMethods" xml:space="preserve">
|
||||||
|
<value>View component '{0}' must have exactly one public method named '{1}' or '{2}'.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewComponent_AsyncMethod_ShouldReturnTask" xml:space="preserve">
|
||||||
|
<value>Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewComponent_CannotFindMethod" xml:space="preserve">
|
||||||
|
<value>Could not find an '{0}' or '{1}' method for the view component '{2}'.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewComponent_SyncMethod_CannotReturnTask" xml:space="preserve">
|
||||||
|
<value>Method '{0}' of view component '{1}' cannot return a {2}.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewComponent_SyncMethod_ShouldReturnValue" xml:space="preserve">
|
||||||
|
<value>Method '{0}' of view component '{1}' should be declared to return a value.</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Razor.Evolution;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||||
|
|
||||||
|
namespace Microsoft.CodeAnalysis.Razor
|
||||||
|
{
|
||||||
|
internal class ViewComponentTagHelperDescriptorFactory
|
||||||
|
{
|
||||||
|
private readonly INamedTypeSymbol _viewComponentAttributeSymbol;
|
||||||
|
private readonly INamedTypeSymbol _genericTaskSymbol;
|
||||||
|
private readonly INamedTypeSymbol _taskSymbol;
|
||||||
|
private readonly INamedTypeSymbol _iDictionarySymbol;
|
||||||
|
|
||||||
|
private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat =
|
||||||
|
SymbolDisplayFormat.FullyQualifiedFormat
|
||||||
|
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
|
||||||
|
.WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
|
||||||
|
|
||||||
|
public ViewComponentTagHelperDescriptorFactory(Compilation compilation)
|
||||||
|
{
|
||||||
|
_viewComponentAttributeSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute);
|
||||||
|
_genericTaskSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.GenericTask);
|
||||||
|
_taskSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.Task);
|
||||||
|
_iDictionarySymbol = compilation.GetTypeByMetadataName(TagHelperTypes.IDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type)
|
||||||
|
{
|
||||||
|
var assemblyName = type.ContainingAssembly.Name;
|
||||||
|
var shortName = GetShortName(type);
|
||||||
|
var tagName = $"vc:{DefaultTagHelperDescriptorFactory.ToHtmlCase(shortName)}";
|
||||||
|
var typeName = $"__Generated__{shortName}ViewComponentTagHelper";
|
||||||
|
|
||||||
|
var descriptor = new TagHelperDescriptor
|
||||||
|
{
|
||||||
|
TagName = tagName,
|
||||||
|
TypeName = typeName,
|
||||||
|
AssemblyName = assemblyName
|
||||||
|
};
|
||||||
|
|
||||||
|
SetAttributeDescriptors(type, descriptor);
|
||||||
|
|
||||||
|
descriptor.PropertyBag.Add(ViewComponentTypes.ViewComponentNameKey, shortName);
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAttributeDescriptors(INamedTypeSymbol type, TagHelperDescriptor descriptor)
|
||||||
|
{
|
||||||
|
var methodParameters = GetInvokeMethodParameters(type);
|
||||||
|
var attributeDescriptors = new List<TagHelperAttributeDescriptor>();
|
||||||
|
var indexerDescriptors = new List<TagHelperAttributeDescriptor>();
|
||||||
|
var requiredAttributeDescriptors = new List<TagHelperRequiredAttributeDescriptor>();
|
||||||
|
|
||||||
|
foreach (var parameter in methodParameters)
|
||||||
|
{
|
||||||
|
var lowerKebabName = DefaultTagHelperDescriptorFactory.ToHtmlCase(parameter.Name);
|
||||||
|
var typeName = parameter.Type.ToDisplayString(FullNameTypeDisplayFormat);
|
||||||
|
var attributeDescriptor = new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = lowerKebabName,
|
||||||
|
PropertyName = parameter.Name,
|
||||||
|
TypeName = typeName
|
||||||
|
};
|
||||||
|
|
||||||
|
attributeDescriptor.IsEnum = parameter.Type.TypeKind == TypeKind.Enum;
|
||||||
|
attributeDescriptor.IsIndexer = false;
|
||||||
|
|
||||||
|
attributeDescriptors.Add(attributeDescriptor);
|
||||||
|
|
||||||
|
var indexerDescriptor = GetIndexerAttributeDescriptor(parameter, lowerKebabName);
|
||||||
|
if (indexerDescriptor != null)
|
||||||
|
{
|
||||||
|
indexerDescriptors.Add(indexerDescriptor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set required attributes only for non-indexer attributes. Indexer attributes can't be required attributes
|
||||||
|
// because there are two ways of setting values for the attribute.
|
||||||
|
requiredAttributeDescriptors.Add(new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = lowerKebabName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeDescriptors.AddRange(indexerDescriptors);
|
||||||
|
descriptor.Attributes = attributeDescriptors;
|
||||||
|
descriptor.RequiredAttributes = requiredAttributeDescriptors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TagHelperAttributeDescriptor GetIndexerAttributeDescriptor(IParameterSymbol parameter, string name)
|
||||||
|
{
|
||||||
|
INamedTypeSymbol dictionaryType;
|
||||||
|
if ((parameter.Type as INamedTypeSymbol)?.ConstructedFrom == _iDictionarySymbol)
|
||||||
|
{
|
||||||
|
dictionaryType = (INamedTypeSymbol)parameter.Type;
|
||||||
|
}
|
||||||
|
else if (parameter.Type.AllInterfaces.Any(s => s.ConstructedFrom == _iDictionarySymbol))
|
||||||
|
{
|
||||||
|
dictionaryType = parameter.Type.AllInterfaces.First(s => s.ConstructedFrom == _iDictionarySymbol);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dictionaryType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dictionaryType == null || dictionaryType.TypeArguments[0].SpecialType != SpecialType.System_String)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = dictionaryType.TypeArguments[1];
|
||||||
|
var descriptor = new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = name + "-",
|
||||||
|
PropertyName = parameter.Name,
|
||||||
|
TypeName = type.ToDisplayString(FullNameTypeDisplayFormat),
|
||||||
|
IsEnum = type.TypeKind == TypeKind.Enum,
|
||||||
|
IsIndexer = true
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImmutableArray<IParameterSymbol> GetInvokeMethodParameters(INamedTypeSymbol componentType)
|
||||||
|
{
|
||||||
|
var methods = componentType.GetMembers()
|
||||||
|
.OfType<IMethodSymbol>()
|
||||||
|
.Where(method =>
|
||||||
|
method.DeclaredAccessibility == Accessibility.Public &&
|
||||||
|
(string.Equals(method.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal) ||
|
||||||
|
string.Equals(method.Name, ViewComponentTypes.SyncMethodName, StringComparison.Ordinal)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (methods.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
ViewComponentResources.FormatViewComponent_CannotFindMethod(ViewComponentTypes.SyncMethodName, ViewComponentTypes.AsyncMethodName, componentType.ToDisplayString(FullNameTypeDisplayFormat)));
|
||||||
|
}
|
||||||
|
else if (methods.Length > 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
ViewComponentResources.FormatViewComponent_AmbiguousMethods(componentType.ToDisplayString(FullNameTypeDisplayFormat), ViewComponentTypes.AsyncMethodName, ViewComponentTypes.SyncMethodName));
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedMethod = methods[0];
|
||||||
|
var returnType = selectedMethod.ReturnType as INamedTypeSymbol;
|
||||||
|
if (string.Equals(selectedMethod.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal) && returnType != null)
|
||||||
|
{
|
||||||
|
if (!returnType.IsGenericType == true ||
|
||||||
|
returnType.ConstructedFrom == _genericTaskSymbol)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(ViewComponentResources.FormatViewComponent_AsyncMethod_ShouldReturnTask(
|
||||||
|
ViewComponentTypes.AsyncMethodName,
|
||||||
|
componentType.ToDisplayString(FullNameTypeDisplayFormat),
|
||||||
|
nameof(Task)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (returnType != null)
|
||||||
|
{
|
||||||
|
// Will invoke synchronously. Method must not return void, Task or Task<T>.
|
||||||
|
if (returnType.SpecialType == SpecialType.System_Void)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(ViewComponentResources.FormatViewComponent_SyncMethod_ShouldReturnValue(
|
||||||
|
ViewComponentTypes.SyncMethodName,
|
||||||
|
componentType.ToDisplayString(FullNameTypeDisplayFormat)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var inheritsFromTask = false;
|
||||||
|
var currentType = returnType;
|
||||||
|
while (currentType != null)
|
||||||
|
{
|
||||||
|
if (currentType == _taskSymbol)
|
||||||
|
{
|
||||||
|
inheritsFromTask = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentType = currentType.BaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inheritsFromTask)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(ViewComponentResources.FormatViewComponent_SyncMethod_CannotReturnTask(
|
||||||
|
ViewComponentTypes.SyncMethodName,
|
||||||
|
componentType.ToDisplayString(FullNameTypeDisplayFormat),
|
||||||
|
nameof(Task)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var methodParameters = selectedMethod.Parameters;
|
||||||
|
|
||||||
|
return methodParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetShortName(INamedTypeSymbol componentType)
|
||||||
|
{
|
||||||
|
var viewComponentAttribute = componentType.GetAttributes().Where(a => a.AttributeClass == _viewComponentAttributeSymbol).FirstOrDefault();
|
||||||
|
var name = viewComponentAttribute
|
||||||
|
?.NamedArguments
|
||||||
|
.Where(namedArgument => string.Equals(namedArgument.Key, ViewComponentTypes.ViewComponent.Name, StringComparison.Ordinal))
|
||||||
|
.FirstOrDefault()
|
||||||
|
.Value
|
||||||
|
.Value as string;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
var separatorIndex = name.LastIndexOf('.');
|
||||||
|
if (separatorIndex >= 0)
|
||||||
|
{
|
||||||
|
return name.Substring(separatorIndex + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get name by convention
|
||||||
|
if (componentType.Name.EndsWith(ViewComponentTypes.ViewComponentSuffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return componentType.Name.Substring(0, componentType.Name.Length - ViewComponentTypes.ViewComponentSuffix.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return componentType.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.CodeAnalysis.Razor
|
||||||
|
{
|
||||||
|
internal static class ViewComponentTypes
|
||||||
|
{
|
||||||
|
public const string Assembly = "Microsoft.AspNetCore.Mvc.ViewFeatures";
|
||||||
|
|
||||||
|
public const string ViewComponentSuffix = "ViewComponent";
|
||||||
|
|
||||||
|
public const string ViewComponentAttribute = "Microsoft.AspNetCore.Mvc.ViewComponentAttribute";
|
||||||
|
|
||||||
|
public const string NonViewComponentAttribute = "Microsoft.AspNetCore.Mvc.NonViewComponentAttribute";
|
||||||
|
|
||||||
|
public const string GenericTask = "System.Threading.Tasks.Task`1";
|
||||||
|
|
||||||
|
public const string Task = "System.Threading.Tasks.Task";
|
||||||
|
|
||||||
|
public const string ViewComponentNameKey = "ViewComponentName";
|
||||||
|
|
||||||
|
public const string AsyncMethodName = "InvokeAsync";
|
||||||
|
|
||||||
|
public const string SyncMethodName = "Invoke";
|
||||||
|
|
||||||
|
public static class ViewComponent
|
||||||
|
{
|
||||||
|
public const string Name = "Name";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,16 +16,20 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
|
|
||||||
private static INamedTypeSymbol ITagHelperSymbol { get; } = Compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper);
|
private static INamedTypeSymbol ITagHelperSymbol { get; } = Compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper);
|
||||||
|
|
||||||
private DefaultTagHelperResolver.Visitor TestVisitor => new DefaultTagHelperResolver.Visitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
|
// In practice MVC will provide a marker attribute for ViewComponents. To prevent a circular reference between MVC and Razor
|
||||||
|
// we can use a test class as a marker.
|
||||||
|
private static INamedTypeSymbol TestViewComponentAttributeSymbol { get; } = Compilation.GetTypeByMetadataName(typeof(TestViewComponentAttribute).FullName);
|
||||||
|
private static INamedTypeSymbol TestNonViewComponentAttributeSymbol { get; } = Compilation.GetTypeByMetadataName(typeof(TestNonViewComponentAttribute).FullName);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsTagHelper_PlainTagHelper_ReturnsTrue()
|
public void IsTagHelper_PlainTagHelper_ReturnsTrue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
|
||||||
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_PlainTagHelper).FullName);
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_PlainTagHelper).FullName);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
|
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(isTagHelper);
|
Assert.True(isTagHelper);
|
||||||
|
|
@ -35,10 +39,11 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
public void IsTagHelper_InheritedTagHelper_ReturnsTrue()
|
public void IsTagHelper_InheritedTagHelper_ReturnsTrue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
|
||||||
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_InheritedTagHelper).FullName);
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_InheritedTagHelper).FullName);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
|
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(isTagHelper);
|
Assert.True(isTagHelper);
|
||||||
|
|
@ -48,10 +53,11 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
public void IsTagHelper_AbstractTagHelper_ReturnsFalse()
|
public void IsTagHelper_AbstractTagHelper_ReturnsFalse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
|
||||||
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_AbstractTagHelper).FullName);
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_AbstractTagHelper).FullName);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
|
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(isTagHelper);
|
Assert.False(isTagHelper);
|
||||||
|
|
@ -61,10 +67,11 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
public void IsTagHelper_GenericTagHelper_ReturnsFalse()
|
public void IsTagHelper_GenericTagHelper_ReturnsFalse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
|
||||||
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_GenericTagHelper<>).FullName);
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_GenericTagHelper<>).FullName);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
|
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(isTagHelper);
|
Assert.False(isTagHelper);
|
||||||
|
|
@ -74,10 +81,11 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
public void IsTagHelper_InternalTagHelper_ReturnsFalse()
|
public void IsTagHelper_InternalTagHelper_ReturnsFalse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
|
||||||
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InternalTagHelper).FullName);
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InternalTagHelper).FullName);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
|
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(isTagHelper);
|
Assert.False(isTagHelper);
|
||||||
|
|
@ -88,19 +96,177 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var resolver = new DefaultTagHelperResolver(designTime: false);
|
var resolver = new DefaultTagHelperResolver(designTime: false);
|
||||||
|
var expectedTypeName = typeof(DefaultTagHelperResolver).FullName + "." + nameof(Invalid_NestedPublicTagHelper);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var descriptors = resolver.GetTagHelpers(Compilation);
|
var descriptors = resolver.GetTagHelpers(Compilation);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var matchingDescriptors = descriptors
|
var matchingDescriptors = descriptors
|
||||||
.Where(descriptor => string.Equals(descriptor.TypeName, typeof(Invalid_NestedPublicTagHelper).FullName, StringComparison.Ordinal));
|
.Where(descriptor => string.Equals(descriptor.TypeName, expectedTypeName, StringComparison.Ordinal));
|
||||||
Assert.Empty(matchingDescriptors);
|
Assert.Empty(matchingDescriptors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsViewComponent_PlainViewComponent_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
|
||||||
|
TestViewComponentAttributeSymbol,
|
||||||
|
TestNonViewComponentAttributeSymbol,
|
||||||
|
new List<INamedTypeSymbol>());
|
||||||
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_PlainViewComponent).FullName);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(isViewComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsViewComponent_DecoratedViewComponent_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
|
||||||
|
TestViewComponentAttributeSymbol,
|
||||||
|
TestNonViewComponentAttributeSymbol,
|
||||||
|
new List<INamedTypeSymbol>());
|
||||||
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_DecoratedVC).FullName);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(isViewComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsViewComponent_InheritedViewComponent_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
|
||||||
|
TestViewComponentAttributeSymbol,
|
||||||
|
TestNonViewComponentAttributeSymbol,
|
||||||
|
new List<INamedTypeSymbol>());
|
||||||
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_InheritedVC).FullName);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(isViewComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsViewComponent_AbstractViewComponent_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
|
||||||
|
TestViewComponentAttributeSymbol,
|
||||||
|
TestNonViewComponentAttributeSymbol,
|
||||||
|
new List<INamedTypeSymbol>());
|
||||||
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_AbstractViewComponent).FullName);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(isViewComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsViewComponent_GenericViewComponent_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
|
||||||
|
TestViewComponentAttributeSymbol,
|
||||||
|
TestNonViewComponentAttributeSymbol,
|
||||||
|
new List<INamedTypeSymbol>());
|
||||||
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_GenericViewComponent<>).FullName);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(isViewComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsViewComponent_InternalViewComponent_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
|
||||||
|
TestViewComponentAttributeSymbol,
|
||||||
|
TestNonViewComponentAttributeSymbol,
|
||||||
|
new List<INamedTypeSymbol>());
|
||||||
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InternalViewComponent).FullName);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(isViewComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetTagHelpers_NestedViewComponentTagHelpersAreFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var resolver = new DefaultTagHelperResolver(
|
||||||
|
designTime: false,
|
||||||
|
viewComponentAssembly: typeof(DefaultTagHelperResolverTest).Assembly.GetName().Name);
|
||||||
|
var expectedTypeName = "__Generated__" + nameof(Valid_NestedPublicViewComponent) + "TagHelper";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptors = resolver.GetTagHelpers(Compilation);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(descriptors, descriptor => string.Equals(descriptor.TypeName, expectedTypeName, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsViewComponent_DecoratedNonViewComponent_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
|
||||||
|
TestViewComponentAttributeSymbol,
|
||||||
|
TestNonViewComponentAttributeSymbol,
|
||||||
|
new List<INamedTypeSymbol>());
|
||||||
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_DecoratedViewComponent).FullName);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(isViewComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsViewComponent_InheritedNonViewComponent_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
|
||||||
|
TestViewComponentAttributeSymbol,
|
||||||
|
TestNonViewComponentAttributeSymbol,
|
||||||
|
new List<INamedTypeSymbol>());
|
||||||
|
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InheritedViewComponent).FullName);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(isViewComponent);
|
||||||
|
}
|
||||||
|
|
||||||
public class Invalid_NestedPublicTagHelper : TagHelper
|
public class Invalid_NestedPublicTagHelper : TagHelper
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Valid_NestedPublicViewComponent
|
||||||
|
{
|
||||||
|
public string Invoke(string foo) => null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class Invalid_AbstractTagHelper : TagHelper
|
public abstract class Invalid_AbstractTagHelper : TagHelper
|
||||||
|
|
@ -122,4 +288,47 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
public class Valid_InheritedTagHelper : Valid_PlainTagHelper
|
public class Valid_InheritedTagHelper : Valid_PlainTagHelper
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract class Invalid_AbstractViewComponent
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Invalid_GenericViewComponent<T>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Invalid_InternalViewComponent
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Valid_PlainViewComponent
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestViewComponent]
|
||||||
|
public class Valid_DecoratedVC
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Valid_InheritedVC : Valid_DecoratedVC
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestNonViewComponent]
|
||||||
|
public class Invalid_DecoratedViewComponent
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestViewComponent]
|
||||||
|
public class Invalid_InheritedViewComponent : Invalid_DecoratedViewComponent
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestViewComponentAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestNonViewComponentAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Razor.Evolution;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.Workspaces.Test;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.Workspaces.Test.Comparers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.CodeAnalysis.Razor.Workspaces
|
||||||
|
{
|
||||||
|
public class ViewComponentTagHelperDescriptorFactoryTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CreateDescriptor_UnderstandsStringParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testCompilation = TestCompilation.Create();
|
||||||
|
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(StringParameterViewComponent).FullName);
|
||||||
|
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
|
||||||
|
var expectedDescriptor = new TagHelperDescriptor
|
||||||
|
{
|
||||||
|
TagName = "vc:string-parameter",
|
||||||
|
TypeName = "__Generated__StringParameterViewComponentTagHelper",
|
||||||
|
AssemblyName = typeof(StringParameterViewComponent).Assembly.GetName().Name,
|
||||||
|
Attributes = new List<TagHelperAttributeDescriptor>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "foo",
|
||||||
|
PropertyName = "foo",
|
||||||
|
TypeName = typeof(string).FullName
|
||||||
|
},
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "bar",
|
||||||
|
PropertyName = "bar",
|
||||||
|
TypeName = typeof(string).FullName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>
|
||||||
|
{
|
||||||
|
new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "foo"
|
||||||
|
},
|
||||||
|
new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expectedDescriptor.PropertyBag.Add(ViewComponentTypes.ViewComponentNameKey, "StringParameter");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptor = factory.CreateDescriptor(viewComponent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDescriptor_UnderstandsVariousParameterTypes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testCompilation = TestCompilation.Create();
|
||||||
|
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(VariousParameterViewComponent).FullName);
|
||||||
|
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
|
||||||
|
var expectedDescriptor = new TagHelperDescriptor
|
||||||
|
{
|
||||||
|
TagName = "vc:various-parameter",
|
||||||
|
TypeName = "__Generated__VariousParameterViewComponentTagHelper",
|
||||||
|
AssemblyName = typeof(VariousParameterViewComponent).Assembly.GetName().Name,
|
||||||
|
Attributes = new List<TagHelperAttributeDescriptor>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "test-enum",
|
||||||
|
PropertyName = "testEnum",
|
||||||
|
TypeName = typeof(VariousParameterViewComponent).FullName + "." + nameof(VariousParameterViewComponent.TestEnum),
|
||||||
|
IsEnum = true
|
||||||
|
},
|
||||||
|
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "test-string",
|
||||||
|
PropertyName = "testString",
|
||||||
|
TypeName = typeof(string).FullName
|
||||||
|
},
|
||||||
|
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "baz",
|
||||||
|
PropertyName = "baz",
|
||||||
|
TypeName = typeof(int).FullName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>
|
||||||
|
{
|
||||||
|
new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "test-enum"
|
||||||
|
},
|
||||||
|
|
||||||
|
new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "test-string"
|
||||||
|
},
|
||||||
|
|
||||||
|
new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "baz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expectedDescriptor.PropertyBag.Add(ViewComponentTypes.ViewComponentNameKey, "VariousParameter");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptor = factory.CreateDescriptor(viewComponent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDescriptor_UnderstandsGenericParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testCompilation = TestCompilation.Create();
|
||||||
|
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(GenericParameterViewComponent).FullName);
|
||||||
|
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
|
||||||
|
var expectedDescriptor = new TagHelperDescriptor
|
||||||
|
{
|
||||||
|
TagName = "vc:generic-parameter",
|
||||||
|
TypeName = "__Generated__GenericParameterViewComponentTagHelper",
|
||||||
|
AssemblyName = typeof(GenericParameterViewComponent).Assembly.GetName().Name,
|
||||||
|
Attributes = new List<TagHelperAttributeDescriptor>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "foo",
|
||||||
|
PropertyName = "Foo",
|
||||||
|
TypeName = "System.Collections.Generic.List<System.String>"
|
||||||
|
},
|
||||||
|
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "bar",
|
||||||
|
PropertyName = "Bar",
|
||||||
|
TypeName = "System.Collections.Generic.Dictionary<System.String, System.Int32>"
|
||||||
|
},
|
||||||
|
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "bar-",
|
||||||
|
PropertyName = "Bar",
|
||||||
|
TypeName = typeof(int).FullName,
|
||||||
|
IsIndexer = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>
|
||||||
|
{
|
||||||
|
new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expectedDescriptor.PropertyBag.Add(ViewComponentTypes.ViewComponentNameKey, "GenericParameter");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptor = factory.CreateDescriptor(viewComponent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StringParameterViewComponent
|
||||||
|
{
|
||||||
|
public string Invoke(string foo, string bar) => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VariousParameterViewComponent
|
||||||
|
{
|
||||||
|
public string Invoke(TestEnum testEnum, string testString, int baz = 5) => null;
|
||||||
|
|
||||||
|
public enum TestEnum
|
||||||
|
{
|
||||||
|
A = 1,
|
||||||
|
B = 2,
|
||||||
|
C = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GenericParameterViewComponent
|
||||||
|
{
|
||||||
|
public string Invoke(List<string> Foo, Dictionary<string, int> Bar) => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue