Add a new THProvider api

This change adds an API for Tag Helper discovery.

I also got rid of the 'design time' flag for the provider as an
experimental change. We need to think through the consequences of this
before committing to it. Right now I've left those tests failing until we
can make a decision.

This change decouples VCTH discovery a bit more, but we're still not ready
to move that into a the MVC extensions assembly. For that we need the
ability to discover the MVC extensibility.
This commit is contained in:
Ryan Nowak 2017-04-27 08:03:12 -07:00
parent 577511945b
commit ad294fb4ba
35 changed files with 851 additions and 538 deletions

View File

@ -10,8 +10,15 @@
<PackageTags>aspnetcore;aspnetcoremvc;cshtml;razor</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Microsoft.CodeAnalysis.Razor\ViewComponentTagHelperDescriptorConventions.cs">
<Link>ViewComponentTagHelperDescriptorConventions.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Microsoft.AspNetCore.Razor.Language/Microsoft.AspNetCore.Razor.Language.csproj" />
<ProjectReference Include="../Microsoft.CodeAnalysis.Razor/Microsoft.CodeAnalysis.Razor.csproj" />
</ItemGroup>
</Project>

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
{

View File

@ -20,10 +20,10 @@ namespace Microsoft.AspNetCore.Razor.Language
var syntaxTree = codeDocument.GetSyntaxTree();
ThrowForMissingDocumentDependency(syntaxTree);
var resolver = Engine.Features.OfType<ITagHelperFeature>().FirstOrDefault()?.Resolver;
if (resolver == null)
var feature = Engine.Features.OfType<ITagHelperFeature>().FirstOrDefault();
if (feature == null)
{
// No resolver, nothing to do.
// No feature, nothing to do.
return;
}
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Razor.Language
visitor.VisitBlock(syntaxTree.Root);
var errorList = new List<RazorDiagnostic>();
var descriptors = (IReadOnlyList<TagHelperDescriptor>)resolver.Resolve(errorList).ToList();
var descriptors = feature.GetDescriptors();
var errorSink = new ErrorSink();
var directives = visitor.Directives;

View File

@ -0,0 +1,40 @@
// 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.Text.RegularExpressions;
namespace Microsoft.AspNetCore.Razor.Language
{
public static class HtmlCase
{
private const string HtmlCaseRegexReplacement = "-$1$2";
// This matches the following AFTER the start of the input string (MATCH).
// Any letter/number followed by an uppercase letter then lowercase letter: 1(Aa), a(Aa), A(Aa)
// Any lowercase letter followed by an uppercase letter: a(A)
// Each match is then prefixed by a "-" via the ToHtmlCase method.
private static readonly Regex HtmlCaseRegex =
new Regex(
"(?<!^)((?<=[a-zA-Z0-9])[A-Z][a-z])|((?<=[a-z])[A-Z])",
RegexOptions.None,
TimeSpan.FromMilliseconds(500));
/// <summary>
/// Converts from pascal/camel case to lower kebab-case.
/// </summary>
/// <example>
/// SomeThing => some-thing
/// capsONInside => caps-on-inside
/// CAPSOnOUTSIDE => caps-on-outside
/// ALLCAPS => allcaps
/// One1Two2Three3 => one1-two2-three3
/// ONE1TWO2THREE3 => one1two2three3
/// First_Second_ThirdHi => first_second_third-hi
/// </example>
public static string ToHtmlCase(string name)
{
return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant();
}
}
}

View File

@ -0,0 +1,12 @@
// 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.AspNetCore.Razor.Language
{
public interface ITagHelperDescriptorProvider : IRazorEngineFeature
{
int Order { get; }
void Execute(TagHelperDescriptorProviderContext context);
}
}

View File

@ -1,12 +1,12 @@
// 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 Microsoft.AspNetCore.Razor.Language.Legacy;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Language
{
public interface ITagHelperFeature : IRazorEngineFeature
{
ITagHelperDescriptorResolver Resolver { get; }
IReadOnlyList<TagHelperDescriptor> GetDescriptors();
}
}

View File

@ -1,15 +0,0 @@
// 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;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
/// <summary>
/// Contract used to resolve <see cref="TagHelperDescriptor"/>s.
/// </summary>
public interface ITagHelperDescriptorResolver
{
IEnumerable<TagHelperDescriptor> Resolve(IList<RazorDiagnostic> errors);
}
}

View File

@ -1,8 +1,6 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.AspNetCore.Razor.Language
{
internal static class RazorDiagnosticFactory

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Legacy;
namespace Microsoft.AspNetCore.Razor.Language
{
@ -16,6 +15,7 @@ namespace Microsoft.AspNetCore.Razor.Language
private static ICollection<char> InvalidNonWhitespaceAllowedChildCharacters { get; } = new HashSet<char>(
new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' });
private string _documentation;
private string _tagOutputHint;
private HashSet<string> _allowedChildTags;

View File

@ -0,0 +1,44 @@
// 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;
namespace Microsoft.AspNetCore.Razor.Language
{
public abstract class TagHelperDescriptorProviderContext
{
public abstract ItemCollection Items { get; }
public abstract ICollection<TagHelperDescriptor> Results { get; }
public static TagHelperDescriptorProviderContext Create()
{
return new DefaultContext(new List<TagHelperDescriptor>());
}
public static TagHelperDescriptorProviderContext Create(ICollection<TagHelperDescriptor> results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
return new DefaultContext(results);
}
private class DefaultContext : TagHelperDescriptorProviderContext
{
public DefaultContext(ICollection<TagHelperDescriptor> results)
{
Results = results;
Items = new DefaultItemCollection();
}
public override ItemCollection Items { get; }
public override ICollection<TagHelperDescriptor> Results { get; }
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor
@ -20,62 +19,26 @@ namespace Microsoft.CodeAnalysis.Razor
{
var descriptors = new List<TagHelperDescriptor>();
VisitTagHelpers(compilation, descriptors);
VisitViewComponents(compilation, descriptors);
var providers = new ITagHelperDescriptorProvider[]
{
new DefaultTagHelperDescriptorProvider() { DesignTime = true, },
new ViewComponentTagHelperDescriptorProvider(),
};
var results = new List<TagHelperDescriptor>();
var context = TagHelperDescriptorProviderContext.Create(results);
context.SetCompilation(compilation);
for (var i = 0; i < providers.Length; i++)
{
var provider = providers[i];
provider.Execute(context);
}
var diagnostics = new List<RazorDiagnostic>();
var resolutionResult = new TagHelperResolutionResult(descriptors, diagnostics);
var resolutionResult = new TagHelperResolutionResult(results, diagnostics);
return resolutionResult;
}
private void VisitTagHelpers(Compilation compilation, List<TagHelperDescriptor> results)
{
var types = new List<INamedTypeSymbol>();
var visitor = TagHelperTypeVisitor.Create(compilation, types);
VisitCompilation(visitor, compilation);
var factory = new DefaultTagHelperDescriptorFactory(compilation, DesignTime);
foreach (var type in types)
{
var descriptor = factory.CreateDescriptor(type);
if (descriptor != null)
{
results.Add(descriptor);
}
}
}
private void VisitViewComponents(Compilation compilation, List<TagHelperDescriptor> results)
{
var types = new List<INamedTypeSymbol>();
var visitor = ViewComponentTypeVisitor.Create(compilation, types);
VisitCompilation(visitor, compilation);
var factory = new ViewComponentTagHelperDescriptorFactory(compilation);
foreach (var type in types)
{
var descriptor = factory.CreateDescriptor(type);
results.Add(descriptor);
}
}
private static void VisitCompilation(SymbolVisitor visitor, Compilation compilation)
{
visitor.Visit(compilation.Assembly.GlobalNamespace);
foreach (var reference in compilation.References)
{
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
{
visitor.Visit(assembly.GlobalNamespace);
}
}
}
}
}

View File

@ -0,0 +1,38 @@
// 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 System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.CSharp;
namespace Microsoft.CodeAnalysis.Razor
{
public class CompilationTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature
{
private ITagHelperDescriptorProvider[] _providers;
private IMetadataReferenceFeature _referenceFeature;
public IReadOnlyList<TagHelperDescriptor> GetDescriptors()
{
var results = new List<TagHelperDescriptor>();
var context = TagHelperDescriptorProviderContext.Create(results);
var compilation = CSharpCompilation.Create("__TagHelpers", references: _referenceFeature.References);
context.SetCompilation(compilation);
for (var i = 0; i < _providers.Length; i++)
{
_providers[i].Execute(context);
}
return results;
}
protected override void OnInitialized()
{
_referenceFeature = Engine.Features.OfType<IMetadataReferenceFeature>().FirstOrDefault();
_providers = Engine.Features.OfType<ITagHelperDescriptorProvider>().OrderBy(f => f.Order).ToArray();
}
}
}

View File

@ -5,9 +5,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis;
namespace Microsoft.CodeAnalysis.Razor
@ -16,17 +14,6 @@ namespace Microsoft.CodeAnalysis.Razor
{
private const string DataDashPrefix = "data-";
private const string TagHelperNameEnding = "TagHelper";
private const string HtmlCaseRegexReplacement = "-$1$2";
// This matches the following AFTER the start of the input string (MATCH).
// Any letter/number followed by an uppercase letter then lowercase letter: 1(Aa), a(Aa), A(Aa)
// Any lowercase letter followed by an uppercase letter: a(A)
// Each match is then prefixed by a "-" via the ToHtmlCase method.
private static readonly Regex HtmlCaseRegex =
new Regex(
"(?<!^)((?<=[a-zA-Z0-9])[A-Z][a-z])|((?<=[a-z])[A-Z])",
RegexOptions.None,
TimeSpan.FromMilliseconds(500));
private readonly INamedTypeSymbol _htmlAttributeNameAttributeSymbol;
private readonly INamedTypeSymbol _htmlAttributeNotBoundAttributeSymbol;
@ -36,9 +23,6 @@ namespace Microsoft.CodeAnalysis.Razor
private readonly INamedTypeSymbol _restrictChildrenAttributeSymbol;
private readonly INamedTypeSymbol _editorBrowsableAttributeSymbol;
public static ICollection<char> InvalidNonWhitespaceNameCharacters { get; } = new HashSet<char>(
new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' });
private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat =
SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
@ -104,7 +88,7 @@ namespace Microsoft.CodeAnalysis.Razor
descriptorBuilder.TagMatchingRule(ruleBuilder =>
{
var htmlCasedName = ToHtmlCase(name);
var htmlCasedName = HtmlCase.ToHtmlCase(name);
ruleBuilder.RequireTagName(htmlCasedName);
});
@ -213,7 +197,7 @@ namespace Microsoft.CodeAnalysis.Razor
string.IsNullOrEmpty((string)attributeNameAttribute.ConstructorArguments[0].Value))
{
hasExplicitName = false;
attributeName = ToHtmlCase(property.Name);
attributeName = HtmlCase.ToHtmlCase(property.Name);
}
else
{
@ -449,23 +433,6 @@ namespace Microsoft.CodeAnalysis.Razor
return false;
}
/// <summary>
/// Converts from pascal/camel case to lower kebab-case.
/// </summary>
/// <example>
/// SomeThing => some-thing
/// capsONInside => caps-on-inside
/// CAPSOnOUTSIDE => caps-on-outside
/// ALLCAPS => allcaps
/// One1Two2Three3 => one1-two2-three3
/// ONE1TWO2THREE3 => one1two2three3
/// First_Second_ThirdHi => first_second_third-hi
/// </example>
internal static string ToHtmlCase(string name)
{
return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant();
}
private static string GetFullName(ITypeSymbol type) => type.ToDisplayString(FullNameTypeDisplayFormat);
}
}

View File

@ -0,0 +1,60 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor
{
public sealed class DefaultTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider
{
public bool DesignTime { get; set; }
public int Order { get; set; }
public void Execute(TagHelperDescriptorProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var compilation = context.GetCompilation();
if (compilation == null)
{
// No compilation, nothing to do.
return;
}
var types = new List<INamedTypeSymbol>();
var visitor = TagHelperTypeVisitor.Create(compilation, types);
// We always visit the global namespace.
visitor.Visit(compilation.Assembly.GlobalNamespace);
foreach (var reference in compilation.References)
{
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
{
if (IsTagHelperAssembly(assembly))
{
visitor.Visit(assembly.GlobalNamespace);
}
}
}
var factory = new DefaultTagHelperDescriptorFactory(compilation, DesignTime);
for (var i = 0; i < types.Count; i++)
{
var descriptor = factory.CreateDescriptor(types[i]);
context.Results.Add(descriptor);
}
}
private bool IsTagHelperAssembly(IAssemblySymbol assembly)
{
return assembly.Name != null && !assembly.Name.StartsWith("System.", StringComparison.Ordinal);
}
}
}

View File

@ -1,35 +0,0 @@
// 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.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.CSharp;
namespace Microsoft.CodeAnalysis.Razor
{
public class DefaultTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature
{
public ITagHelperDescriptorResolver Resolver { get; private set; }
protected override void OnInitialized()
{
Resolver = new InnerResolver(GetRequiredFeature<IMetadataReferenceFeature>());
}
private class InnerResolver : ITagHelperDescriptorResolver
{
private readonly IMetadataReferenceFeature _referenceFeature;
public InnerResolver(IMetadataReferenceFeature referenceFeature)
{
_referenceFeature = referenceFeature;
}
public IEnumerable<TagHelperDescriptor> Resolve(IList<RazorDiagnostic> errors)
{
var compilation = CSharpCompilation.Create("__TagHelpers", references: _referenceFeature.References);
return TagHelpers.GetTagHelpers(compilation);
}
}
}
}

View File

@ -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.
using System;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor
{
public static class TagHelperDescriptorProviderContextExtensions
{
public static Compilation GetCompilation(this TagHelperDescriptorProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return (Compilation)context.Items[typeof(Compilation)];
}
public static void SetCompilation(this TagHelperDescriptorProviderContext context, Compilation compilation)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.Items[typeof(Compilation)] = compilation;
}
}
}

View File

@ -1,83 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using System;
using System.Collections.Generic;
namespace Microsoft.CodeAnalysis.Razor
{
internal static class TagHelpers
{
public static IReadOnlyList<TagHelperDescriptor> GetTagHelpers(Compilation compilation)
{
var results = new List<TagHelperDescriptor>();
var errors = new ErrorSink();
VisitTagHelpers(compilation, results, errors);
VisitViewComponents(compilation, results, errors);
return results;
}
private static void VisitTagHelpers(Compilation compilation, List<TagHelperDescriptor> results, ErrorSink errors)
{
var types = new List<INamedTypeSymbol>();
var visitor = TagHelperTypeVisitor.Create(compilation, types);
VisitCompilation(visitor, compilation);
var factory = new DefaultTagHelperDescriptorFactory(compilation, designTime: false);
foreach (var type in types)
{
var descriptor = factory.CreateDescriptor(type);
if (descriptor != null)
{
results.Add(descriptor);
}
}
}
private static void VisitViewComponents(Compilation compilation, List<TagHelperDescriptor> results, ErrorSink errors)
{
var types = new List<INamedTypeSymbol>();
var visitor = ViewComponentTypeVisitor.Create(compilation, types);
VisitCompilation(visitor, compilation);
var factory = new ViewComponentTagHelperDescriptorFactory(compilation);
foreach (var type in types)
{
try
{
var descriptor = factory.CreateDescriptor(type);
if (descriptor != null)
{
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);
foreach (var reference in compilation.References)
{
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
{
visitor.Visit(assembly.GlobalNamespace);
}
}
}
}
}

View File

@ -0,0 +1,102 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor
{
internal class ViewComponentDiagnosticFactory
{
private const string DiagnosticPrefix = "RZ";
public static readonly RazorDiagnosticDescriptor ViewComponent_CannotFindMethod =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}3900",
() => ViewComponentResources.ViewComponent_CannotFindMethod,
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic CreateViewComponent_CannotFindMethod(string tagHelperType)
{
var diagnostic = RazorDiagnostic.Create(
ViewComponent_CannotFindMethod,
new SourceSpan(SourceLocation.Undefined, contentLength: 0),
ViewComponentTypes.SyncMethodName,
ViewComponentTypes.AsyncMethodName,
tagHelperType);
return diagnostic;
}
public static readonly RazorDiagnosticDescriptor ViewComponent_AmbiguousMethods =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}3901",
() => ViewComponentResources.ViewComponent_AmbiguousMethods,
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic CreateViewComponent_AmbiguousMethods(string tagHelperType)
{
var diagnostic = RazorDiagnostic.Create(
ViewComponent_AmbiguousMethods,
new SourceSpan(SourceLocation.Undefined, contentLength: 0),
tagHelperType,
ViewComponentTypes.SyncMethodName,
ViewComponentTypes.AsyncMethodName);
return diagnostic;
}
public static readonly RazorDiagnosticDescriptor ViewComponent_AsyncMethod_ShouldReturnTask =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}3902",
() => ViewComponentResources.ViewComponent_AsyncMethod_ShouldReturnTask,
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic CreateViewComponent_AsyncMethod_ShouldReturnTask(string tagHelperType)
{
var diagnostic = RazorDiagnostic.Create(
ViewComponent_AsyncMethod_ShouldReturnTask,
new SourceSpan(SourceLocation.Undefined, contentLength: 0),
ViewComponentTypes.AsyncMethodName,
tagHelperType,
nameof(Task));
return diagnostic;
}
public static readonly RazorDiagnosticDescriptor ViewComponent_SyncMethod_ShouldReturnValue =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}3903",
() => ViewComponentResources.ViewComponent_SyncMethod_ShouldReturnValue,
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic CreateViewComponent_SyncMethod_ShouldReturnValue(string tagHelperType)
{
var diagnostic = RazorDiagnostic.Create(
ViewComponent_SyncMethod_ShouldReturnValue,
new SourceSpan(SourceLocation.Undefined, contentLength: 0),
ViewComponentTypes.SyncMethodName,
tagHelperType);
return diagnostic;
}
public static readonly RazorDiagnosticDescriptor ViewComponent_SyncMethod_CannotReturnTask =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}3904",
() => ViewComponentResources.ViewComponent_SyncMethod_CannotReturnTask,
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic CreateViewComponent_SyncMethod_CannotReturnTask(string tagHelperType)
{
var diagnostic = RazorDiagnostic.Create(
ViewComponent_SyncMethod_CannotReturnTask,
new SourceSpan(SourceLocation.Undefined, contentLength: 0),
ViewComponentTypes.SyncMethodName,
tagHelperType,
nameof(Task));
return diagnostic;
}
}
}

View File

@ -3,12 +3,12 @@
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
namespace Microsoft.CodeAnalysis.Razor
{
/// <summary>
/// A library of methods used to generate <see cref="TagHelperDescriptor"/>s for view components.
/// </summary>
public static class ViewComponentTagHelperDescriptorConventions
internal static class ViewComponentTagHelperDescriptorConventions
{
/// <summary>
/// The key in a <see cref="TagHelperDescriptor.Metadata"/> containing

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor
@ -26,24 +25,32 @@ namespace Microsoft.CodeAnalysis.Razor
_viewComponentAttributeSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute);
_genericTaskSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.GenericTask);
_taskSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.Task);
_iDictionarySymbol = compilation.GetTypeByMetadataName(TagHelperTypes.IDictionary);
_iDictionarySymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.IDictionary);
}
public virtual TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type)
{
var assemblyName = type.ContainingAssembly.Name;
var shortName = GetShortName(type);
var tagName = $"vc:{DefaultTagHelperDescriptorFactory.ToHtmlCase(shortName)}";
var tagName = $"vc:{HtmlCase.ToHtmlCase(shortName)}";
var typeName = $"__Generated__{shortName}ViewComponentTagHelper";
var descriptorBuilder = TagHelperDescriptorBuilder.Create(typeName, assemblyName);
var methodParameters = GetInvokeMethodParameters(type);
descriptorBuilder.TagMatchingRule(ruleBuilder =>
{
ruleBuilder.RequireTagName(tagName);
AddRequiredAttributes(methodParameters, ruleBuilder);
});
AddBoundAttributes(methodParameters, descriptorBuilder);
if (TryFindInvokeMethod(type, out var method, out var diagnostic))
{
var methodParameters = method.Parameters;
descriptorBuilder.TagMatchingRule(ruleBuilder =>
{
ruleBuilder.RequireTagName(tagName);
AddRequiredAttributes(methodParameters, ruleBuilder);
});
AddBoundAttributes(methodParameters, descriptorBuilder);
}
else
{
descriptorBuilder.AddDiagnostic(diagnostic);
}
descriptorBuilder.AddMetadata(ViewComponentTypes.ViewComponentNameKey, shortName);
@ -51,6 +58,77 @@ namespace Microsoft.CodeAnalysis.Razor
return descriptor;
}
private bool TryFindInvokeMethod(INamedTypeSymbol type, out IMethodSymbol method, out RazorDiagnostic diagnostic)
{
var methods = type.GetMembers()
.OfType<IMethodSymbol>()
.Where(m =>
m.DeclaredAccessibility == Accessibility.Public &&
(string.Equals(m.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal) ||
string.Equals(m.Name, ViewComponentTypes.SyncMethodName, StringComparison.Ordinal)))
.ToArray();
if (methods.Length == 0)
{
diagnostic = ViewComponentDiagnosticFactory.CreateViewComponent_CannotFindMethod(type.ToDisplayString(FullNameTypeDisplayFormat));
method = null;
return false;
}
else if (methods.Length > 1)
{
diagnostic = ViewComponentDiagnosticFactory.CreateViewComponent_AmbiguousMethods(type.ToDisplayString(FullNameTypeDisplayFormat));
method = null;
return false;
}
var selectedMethod = methods[0];
var returnType = selectedMethod.ReturnType as INamedTypeSymbol;
if (string.Equals(selectedMethod.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal))
{
// Will invoke asynchronously. Method must not return Task or Task<T>.
if (returnType == _taskSymbol)
{
// This is ok.
}
else if (returnType.IsGenericType && returnType.ConstructedFrom == _genericTaskSymbol)
{
// This is ok.
}
else
{
diagnostic = ViewComponentDiagnosticFactory.CreateViewComponent_AsyncMethod_ShouldReturnTask(type.ToDisplayString(FullNameTypeDisplayFormat));
method = null;
return false;
}
}
else
{
// Will invoke synchronously. Method must not return void, Task or Task<T>.
if (returnType.SpecialType == SpecialType.System_Void)
{
diagnostic = ViewComponentDiagnosticFactory.CreateViewComponent_SyncMethod_ShouldReturnValue(type.ToDisplayString(FullNameTypeDisplayFormat));
method = null;
return false;
}
else if (returnType == _taskSymbol)
{
diagnostic = ViewComponentDiagnosticFactory.CreateViewComponent_SyncMethod_CannotReturnTask(type.ToDisplayString(FullNameTypeDisplayFormat));
method = null;
return false;
}
else if (returnType.IsGenericType && returnType.ConstructedFrom == _genericTaskSymbol)
{
diagnostic = ViewComponentDiagnosticFactory.CreateViewComponent_SyncMethod_CannotReturnTask(type.ToDisplayString(FullNameTypeDisplayFormat));
method = null;
return false;
}
}
method = selectedMethod;
diagnostic = null;
return true;
}
private void AddRequiredAttributes(ImmutableArray<IParameterSymbol> methodParameters, TagMatchingRuleBuilder builder)
{
foreach (var parameter in methodParameters)
@ -61,7 +139,7 @@ namespace Microsoft.CodeAnalysis.Razor
// because there are two ways of setting values for the attribute.
builder.RequireAttribute(attributeBuilder =>
{
var lowerKebabName = DefaultTagHelperDescriptorFactory.ToHtmlCase(parameter.Name);
var lowerKebabName = HtmlCase.ToHtmlCase(parameter.Name);
attributeBuilder.Name(lowerKebabName);
});
}
@ -72,7 +150,7 @@ namespace Microsoft.CodeAnalysis.Razor
{
foreach (var parameter in methodParameters)
{
var lowerKebabName = DefaultTagHelperDescriptorFactory.ToHtmlCase(parameter.Name);
var lowerKebabName = HtmlCase.ToHtmlCase(parameter.Name);
var typeName = parameter.Type.ToDisplayString(FullNameTypeDisplayFormat);
builder.BindAttribute(attributeBuilder =>
{
@ -124,77 +202,6 @@ namespace Microsoft.CodeAnalysis.Razor
return typeName;
}
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();

View File

@ -0,0 +1,65 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor
{
public sealed class ViewComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider
{
// Hack for testability. The visitor will normally just no op if we're not referencing
// an appropriate version of MVC.
internal bool ForceEnabled { get; set; }
public int Order { get; set; }
public void Execute(TagHelperDescriptorProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var compilation = context.GetCompilation();
if (compilation == null)
{
// No compilation, nothing to do.
return;
}
var types = new List<INamedTypeSymbol>();
var visitor = ViewComponentTypeVisitor.Create(compilation, types);
if (ForceEnabled)
{
visitor.Enabled = true;
}
// We always visit the global namespace.
visitor.Visit(compilation.Assembly.GlobalNamespace);
foreach (var reference in compilation.References)
{
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
{
if (IsTagHelperAssembly(assembly))
{
visitor.Visit(assembly.GlobalNamespace);
}
}
}
var factory = new ViewComponentTagHelperDescriptorFactory(compilation);
for (var i = 0; i < types.Count; i++)
{
context.Results.Add(factory.CreateDescriptor(types[i]));
}
}
private bool IsTagHelperAssembly(IAssemblySymbol assembly)
{
return assembly.Name != null && !assembly.Name.StartsWith("System.", StringComparison.Ordinal);
}
}
}

View File

@ -11,9 +11,9 @@ namespace Microsoft.CodeAnalysis.Razor
{
private static readonly Version SupportedVCTHMvcVersion = new Version(1, 1);
private INamedTypeSymbol _viewComponentAttribute;
private INamedTypeSymbol _nonViewComponentAttribute;
private List<INamedTypeSymbol> _results;
private readonly INamedTypeSymbol _viewComponentAttribute;
private readonly INamedTypeSymbol _nonViewComponentAttribute;
private readonly List<INamedTypeSymbol> _results;
public static ViewComponentTypeVisitor Create(Compilation compilation, List<INamedTypeSymbol> results)
{
@ -45,8 +45,12 @@ namespace Microsoft.CodeAnalysis.Razor
_viewComponentAttribute = viewComponentAttribute;
_nonViewComponentAttribute = nonViewComponentAttribute;
_results = results;
Enabled = _viewComponentAttribute != null;
}
public bool Enabled { get; set; }
public override void VisitNamedType(INamedTypeSymbol symbol)
{
if (IsViewComponent(symbol))
@ -75,7 +79,7 @@ namespace Microsoft.CodeAnalysis.Razor
internal bool IsViewComponent(INamedTypeSymbol symbol)
{
if (_viewComponentAttribute == null)
if (!Enabled)
{
return false;
}
@ -94,7 +98,7 @@ namespace Microsoft.CodeAnalysis.Razor
private static bool AttributeIsDefined(INamedTypeSymbol type, INamedTypeSymbol queryAttribute)
{
if (type == null)
if (type == null || queryAttribute == null)
{
return false;
}

View File

@ -21,6 +21,8 @@ namespace Microsoft.CodeAnalysis.Razor
public const string Task = "System.Threading.Tasks.Task";
public const string IDictionary = "System.Collections.Generic.IDictionary`2";
public const string ViewComponentNameKey = "ViewComponentName";
public const string AsyncMethodName = "InvokeAsync";

View File

@ -505,7 +505,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.IntegrationTests
}
else
{
b.Features.Add(new DefaultTagHelperFeature());
b.Features.Add(new CompilationTagHelperFeature());
b.Features.Add(new DefaultTagHelperDescriptorProvider() { DesignTime = true });
b.Features.Add(new ViewComponentTagHelperDescriptorProvider());
}
});
}
@ -524,7 +526,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.IntegrationTests
}
else
{
b.Features.Add(new DefaultTagHelperFeature());
b.Features.Add(new CompilationTagHelperFeature());
b.Features.Add(new DefaultTagHelperDescriptorProvider());
b.Features.Add(new ViewComponentTagHelperDescriptorProvider());
}
});
}

View File

@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
{
return RazorEngine.Create(b =>
{
b.Features.Add(new TagHelperFeature(tagHelpers));
b.Features.Add(new TestTagHelperFeature(tagHelpers));
});
}
@ -205,30 +205,5 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
Node = node;
}
}
private class TagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature
{
public TagHelperFeature(TagHelperDescriptor[] tagHelpers)
{
Resolver = new TagHelperDescriptorResolver(tagHelpers);
}
public ITagHelperDescriptorResolver Resolver { get; }
}
private class TagHelperDescriptorResolver : ITagHelperDescriptorResolver
{
public TagHelperDescriptorResolver(TagHelperDescriptor[] tagHelpers)
{
TagHelpers = tagHelpers;
}
public TagHelperDescriptor[] TagHelpers { get; }
public IEnumerable<TagHelperDescriptor> Resolve(IList<RazorDiagnostic> errors)
{
return TagHelpers;
}
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions

View File

@ -1,12 +1,10 @@
// 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 System.IO;
using System.Text;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Razor;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
@ -295,7 +293,7 @@ public class __Generated__TagCloudViewComponentTagHelper : Microsoft.AspNetCore.
{
b.Features.Add(new MvcViewDocumentClassifierPass());
b.Features.Add(new TagHelperFeature(tagHelpers));
b.Features.Add(new TestTagHelperFeature(tagHelpers));
});
}
@ -363,30 +361,5 @@ public class __Generated__TagCloudViewComponentTagHelper : Microsoft.AspNetCore.
Node = node;
}
}
private class TagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature
{
public TagHelperFeature(TagHelperDescriptor[] tagHelpers)
{
Resolver = new TagHelperDescriptorResolver(tagHelpers);
}
public ITagHelperDescriptorResolver Resolver { get; }
}
private class TagHelperDescriptorResolver : ITagHelperDescriptorResolver
{
public TagHelperDescriptorResolver(TagHelperDescriptor[] tagHelpers)
{
TagHelpers = tagHelpers;
}
public TagHelperDescriptor[] TagHelpers { get; }
public IEnumerable<TagHelperDescriptor> Resolve(IList<RazorDiagnostic> errors)
{
return TagHelpers;
}
}
}
}

View File

@ -318,12 +318,11 @@ namespace Microsoft.AspNetCore.Razor.Language
}
[Fact]
public void Execute_NoopsWhenNoResolver()
public void Execute_NoopsWhenNoFeature()
{
// Arrange
var engine = RazorEngine.Create(builder =>
{
builder.Features.Add(Mock.Of<ITagHelperFeature>());
});
var phase = new DefaultRazorTagHelperBinderPhase()
{
@ -401,76 +400,6 @@ namespace Microsoft.AspNetCore.Razor.Language
Assert.Empty(context.TagHelpers);
}
[Fact]
public void Execute_RecreatesSyntaxTreeOnResolverErrors()
{
// Arrange
var resolverError = RazorDiagnostic.Create(new RazorError("Test error", new SourceLocation(19, 1, 17), length: 12));
var engine = RazorEngine.Create(builder =>
{
var resolver = new ErrorLoggingTagHelperDescriptorResolver(resolverError, tagName: "test");
builder.Features.Add(Mock.Of<ITagHelperFeature>(f => f.Resolver == resolver));
});
var phase = new DefaultRazorTagHelperBinderPhase()
{
Engine = engine,
};
var sourceDocument = CreateTestSourceDocument();
var codeDocument = RazorCodeDocument.Create(sourceDocument);
var originalTree = RazorSyntaxTree.Parse(sourceDocument);
var initialError = RazorDiagnostic.Create(new RazorError("Initial test error", SourceLocation.Zero, length: 1));
var erroredOriginalTree = RazorSyntaxTree.Create(
originalTree.Root,
originalTree.Source,
new[] { initialError },
originalTree.Options);
codeDocument.SetSyntaxTree(erroredOriginalTree);
// Act
phase.Execute(codeDocument);
// Assert
var outputTree = codeDocument.GetSyntaxTree();
Assert.Empty(originalTree.Diagnostics);
Assert.NotSame(erroredOriginalTree, outputTree);
Assert.Equal(new[] { initialError, resolverError }, outputTree.Diagnostics);
}
[Fact]
public void Execute_CombinesDiagnosticsFromTagHelperDescriptor()
{
// Arrange
var resolverError = RazorDiagnostic.Create(new RazorError("Test error", new SourceLocation(19, 1, 17), length: 12));
var engine = RazorEngine.Create(builder =>
{
var resolver = new ErrorLoggingTagHelperDescriptorResolver(resolverError, tagName: null);
builder.Features.Add(Mock.Of<ITagHelperFeature>(f => f.Resolver == resolver));
});
var descriptorError = RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedTagNameNullOrWhitespace();
var phase = new DefaultRazorTagHelperBinderPhase()
{
Engine = engine,
};
var sourceDocument = CreateTestSourceDocument();
var codeDocument = RazorCodeDocument.Create(sourceDocument);
var originalTree = RazorSyntaxTree.Parse(sourceDocument);
codeDocument.SetSyntaxTree(originalTree);
// Act
phase.Execute(codeDocument);
// Assert
var outputTree = codeDocument.GetSyntaxTree();
Assert.Empty(originalTree.Diagnostics);
Assert.Equal(new[] { resolverError, descriptorError }, outputTree.Diagnostics);
}
[Fact]
public void Execute_CombinesErrorsOnRewritingErrors()
{
@ -1490,27 +1419,5 @@ namespace Microsoft.AspNetCore.Razor.Language
return descriptor;
}
private class ErrorLoggingTagHelperDescriptorResolver : ITagHelperDescriptorResolver
{
private readonly RazorDiagnostic _error;
private readonly string _tagName;
public ErrorLoggingTagHelperDescriptorResolver(RazorDiagnostic error, string tagName = null)
{
_error = error;
_tagName = tagName;
}
public IEnumerable<TagHelperDescriptor> Resolve(IList<RazorDiagnostic> errors)
{
errors.Add(_error);
return new[] { CreateTagHelperDescriptor(
tagName: _tagName,
typeName: null,
assemblyName: "TestAssembly") };
}
}
}
}

View File

@ -0,0 +1,39 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Razor.Language
{
public class HtmlCaseTest
{
public static TheoryData HtmlConversionData
{
get
{
return new TheoryData<string, string>
{
{ "SomeThing", "some-thing" },
{ "someOtherThing", "some-other-thing" },
{ "capsONInside", "caps-on-inside" },
{ "CAPSOnOUTSIDE", "caps-on-outside" },
{ "ALLCAPS", "allcaps" },
{ "One1Two2Three3", "one1-two2-three3" },
{ "ONE1TWO2THREE3", "one1two2three3" },
{ "First_Second_ThirdHi", "first_second_third-hi" }
};
}
}
[Theory]
[MemberData(nameof(HtmlConversionData))]
public void ToHtmlCase_ReturnsExpectedConversions(string input, string expectedOutput)
{
// Arrange, Act
var output = HtmlCase.ToHtmlCase(input);
// Assert
Assert.Equal(output, expectedOutput);
}
}
}

View File

@ -1,27 +0,0 @@
// 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.Language.Legacy;
namespace Microsoft.AspNetCore.Razor.Language
{
internal class TestTagHelperDescriptorResolver : ITagHelperDescriptorResolver
{
public TestTagHelperDescriptorResolver()
{
}
public TestTagHelperDescriptorResolver(IEnumerable<TagHelperDescriptor> tagHelpers)
{
TagHelpers.AddRange(tagHelpers);
}
public List<TagHelperDescriptor> TagHelpers { get; } = new List<TagHelperDescriptor>();
public IEnumerable<TagHelperDescriptor> Resolve(IList<RazorDiagnostic> errors)
{
return TagHelpers;
}
}
}

View File

@ -1,7 +1,6 @@
// 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 Microsoft.AspNetCore.Razor.Language.Legacy;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Language
@ -10,16 +9,19 @@ namespace Microsoft.AspNetCore.Razor.Language
{
public TestTagHelperFeature()
{
Resolver = new TestTagHelperDescriptorResolver();
TagHelpers = new List<TagHelperDescriptor>();
}
public TestTagHelperFeature(IEnumerable<TagHelperDescriptor> tagHelpers)
{
Resolver = new TestTagHelperDescriptorResolver(tagHelpers);
TagHelpers = new List<TagHelperDescriptor>(tagHelpers);
}
public List<TagHelperDescriptor> TagHelpers => ((TestTagHelperDescriptorResolver)Resolver).TagHelpers;
public List<TagHelperDescriptor> TagHelpers { get; }
public ITagHelperDescriptorResolver Resolver { get; }
public IReadOnlyList<TagHelperDescriptor> GetDescriptors()
{
return TagHelpers.ToArray();
}
}
}

View File

@ -1970,35 +1970,6 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
Assert.Equal(expectedDiagnostics, descriptor.GetAllDiagnostics());
}
public static TheoryData HtmlConversionData
{
get
{
return new TheoryData<string, string>
{
{ "SomeThing", "some-thing" },
{ "someOtherThing", "some-other-thing" },
{ "capsONInside", "caps-on-inside" },
{ "CAPSOnOUTSIDE", "caps-on-outside" },
{ "ALLCAPS", "allcaps" },
{ "One1Two2Three3", "one1-two2-three3" },
{ "ONE1TWO2THREE3", "one1two2three3" },
{ "First_Second_ThirdHi", "first_second_third-hi" }
};
}
}
[Theory]
[MemberData(nameof(HtmlConversionData))]
public void ToHtmlCase_ReturnsExpectedConversions(string input, string expectedOutput)
{
// Arrange, Act
var output = DefaultTagHelperDescriptorFactory.ToHtmlCase(input);
// Assert
Assert.Equal(output, expectedOutput);
}
public static TheoryData TagOutputHintData
{
get

View File

@ -14,6 +14,29 @@ namespace Microsoft.CodeAnalysis.Razor
{
public static class TestCompilation
{
private static IEnumerable<MetadataReference> _metadataReferences;
public static IEnumerable<MetadataReference> MetadataReferences
{
get
{
if (_metadataReferences == null)
{
var currentAssembly = typeof(TestCompilation).GetTypeInfo().Assembly;
var dependencyContext = DependencyContext.Load(currentAssembly);
_metadataReferences = dependencyContext.CompileLibraries
.SelectMany(l => l.ResolveReferencePaths())
.Select(assemblyPath => MetadataReference.CreateFromFile(assemblyPath))
.ToArray();
}
return _metadataReferences;
}
}
public static string AssemblyName => "TestAssembly";
public static Compilation Create(SyntaxTree syntaxTree = null)
{
IEnumerable<SyntaxTree> syntaxTrees = null;
@ -23,12 +46,7 @@ namespace Microsoft.CodeAnalysis.Razor
syntaxTrees = new[] { syntaxTree };
}
var currentAssembly = typeof(TestCompilation).GetTypeInfo().Assembly;
var dependencyContext = DependencyContext.Load(currentAssembly);
var references = dependencyContext.CompileLibraries.SelectMany(l => l.ResolveReferencePaths())
.Select(assemblyPath => MetadataReference.CreateFromFile(assemblyPath));
var compilation = CSharpCompilation.Create("TestAssembly", syntaxTrees, references);
var compilation = CSharpCompilation.Create(AssemblyName, syntaxTrees, MetadataReferences);
EnsureValidCompilation(compilation);

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Reflection;
using Xunit;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using System.Threading.Tasks;
namespace Microsoft.CodeAnalysis.Razor.Workspaces
{
@ -122,6 +123,140 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
// Assert
Assert.Equal(expectedDescriptor, descriptor, TagHelperDescriptorComparer.CaseSensitive);
}
[Fact]
public void CreateDescriptor_AddsDiagnostic_ForViewComponentWithNoInvokeMethod()
{
// Arrange
var testCompilation = TestCompilation.Create();
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(ViewComponentWithoutInvokeMethod).FullName);
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
var diagnostic = Assert.Single(descriptor.GetAllDiagnostics());
Assert.Equal(ViewComponentDiagnosticFactory.ViewComponent_CannotFindMethod.Id, diagnostic.Id);
}
[Fact]
public void CreateDescriptor_ForViewComponentWithInvokeAsync_UnderstandsGenericTask()
{
// Arrange
var testCompilation = TestCompilation.Create();
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(AsyncViewComponentWithGenericTask).FullName);
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
Assert.Empty(descriptor.GetAllDiagnostics());
}
[Fact]
public void CreateDescriptor_ForViewComponentWithInvokeAsync_UnderstandsNonGenericTask()
{
// Arrange
var testCompilation = TestCompilation.Create();
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(AsyncViewComponentWithNonGenericTask).FullName);
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
Assert.Empty(descriptor.GetAllDiagnostics());
}
[Fact]
public void CreateDescriptor_ForViewComponentWithInvokeAsync_DoesNotUnderstandVoid()
{
// Arrange
var testCompilation = TestCompilation.Create();
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(AsyncViewComponentWithString).FullName);
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
var diagnostic = Assert.Single(descriptor.GetAllDiagnostics());
Assert.Equal(ViewComponentDiagnosticFactory.ViewComponent_AsyncMethod_ShouldReturnTask.Id, diagnostic.Id);
}
[Fact]
public void CreateDescriptor_ForViewComponentWithInvokeAsync_DoesNotUnderstandString()
{
// Arrange
var testCompilation = TestCompilation.Create();
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(AsyncViewComponentWithString).FullName);
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
var diagnostic = Assert.Single(descriptor.GetAllDiagnostics());
Assert.Equal(ViewComponentDiagnosticFactory.ViewComponent_AsyncMethod_ShouldReturnTask.Id, diagnostic.Id);
}
[Fact]
public void CreateDescriptor_ForViewComponentWithInvoke_DoesNotUnderstandVoid()
{
// Arrange
var testCompilation = TestCompilation.Create();
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(SyncViewComponentWithVoid).FullName);
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
var diagnostic = Assert.Single(descriptor.GetAllDiagnostics());
Assert.Equal(ViewComponentDiagnosticFactory.ViewComponent_SyncMethod_ShouldReturnValue.Id, diagnostic.Id);
}
[Fact]
public void CreateDescriptor_ForViewComponentWithInvoke_DoesNotUnderstandNonGenericTask()
{
// Arrange
var testCompilation = TestCompilation.Create();
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(SyncViewComponentWithNonGenericTask).FullName);
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
var diagnostic = Assert.Single(descriptor.GetAllDiagnostics());
Assert.Equal(ViewComponentDiagnosticFactory.ViewComponent_SyncMethod_CannotReturnTask.Id, diagnostic.Id);
}
[Fact]
public void CreateDescriptor_ForViewComponentWithInvoke_DoesNotUnderstandGenericTask()
{
// Arrange
var testCompilation = TestCompilation.Create();
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(SyncViewComponentWithGenericTask).FullName);
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
var diagnostic = Assert.Single(descriptor.GetAllDiagnostics());
Assert.Equal(ViewComponentDiagnosticFactory.ViewComponent_SyncMethod_CannotReturnTask.Id, diagnostic.Id);
}
}
public class StringParameterViewComponent
@ -145,4 +280,43 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
{
public string Invoke(List<string> Foo, Dictionary<string, int> Bar) => null;
}
public class ViewComponentWithoutInvokeMethod
{
}
public class AsyncViewComponentWithGenericTask
{
public Task<string> InvokeAsync() => null;
}
public class AsyncViewComponentWithNonGenericTask
{
public Task InvokeAsync() => null;
}
public class AsyncViewComponentWithVoid
{
public void InvokeAsync() { }
}
public class AsyncViewComponentWithString
{
public string InvokeAsync() => null;
}
public class SyncViewComponentWithVoid
{
public void Invoke() { }
}
public class SyncViewComponentWithNonGenericTask
{
public Task Invoke() => null;
}
public class SyncViewComponentWithGenericTask
{
public Task<string> Invoke() => null;
}
}

View File

@ -0,0 +1,68 @@
// 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.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor
{
// This is just a basic integration test. There are detailed tests for the VCTH visitor and descriptor factory.
public class ViewComponentTagHelperDescriptorProviderTest
{
[Fact]
public void DescriptorProvider_FindsVCTH()
{
// Arrange
var code = @"
public class StringParameterViewComponent
{
public string Invoke(string foo, string bar) => null;
}
";
var testCompilation = TestCompilation.Create(CSharpSyntaxTree.ParseText(code));
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(testCompilation);
var provider = new ViewComponentTagHelperDescriptorProvider()
{
Engine = RazorEngine.CreateEmpty(b => { }),
ForceEnabled = true,
};
var expectedDescriptor = TagHelperDescriptorBuilder.Create(
"__Generated__StringParameterViewComponentTagHelper",
TestCompilation.AssemblyName)
.TagMatchingRule(rule =>
rule
.RequireTagName("vc:string-parameter")
.RequireAttribute(attribute => attribute.Name("foo"))
.RequireAttribute(attribute => attribute.Name("bar")))
.BindAttribute(attribute =>
attribute
.Name("foo")
.PropertyName("foo")
.TypeName(typeof(string).FullName))
.BindAttribute(attribute =>
attribute
.Name("bar")
.PropertyName("bar")
.TypeName(typeof(string).FullName))
.AddMetadata(ViewComponentTypes.ViewComponentNameKey, "StringParameter")
.Build();
// Act
provider.Execute(context);
// Assert
var descriptor = context.Results.FirstOrDefault(d => TagHelperDescriptorComparer.CaseSensitive.Equals(d, expectedDescriptor));
Assert.NotNull(descriptor);
}
}
}