diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj index 33ede5aecf..66f55496b0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj @@ -10,8 +10,15 @@ aspnetcore;aspnetcoremvc;cshtml;razor + + + ViewComponentTagHelperDescriptorConventions.cs + + + + diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperPass.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperPass.cs index 396e4dab48..7b78770d54 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperPass.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperPass.cs @@ -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 { diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs index 6221179257..a692fba5ee 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs @@ -20,10 +20,10 @@ namespace Microsoft.AspNetCore.Razor.Language var syntaxTree = codeDocument.GetSyntaxTree(); ThrowForMissingDocumentDependency(syntaxTree); - var resolver = Engine.Features.OfType().FirstOrDefault()?.Resolver; - if (resolver == null) + var feature = Engine.Features.OfType().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(); - var descriptors = (IReadOnlyList)resolver.Resolve(errorList).ToList(); + var descriptors = feature.GetDescriptors(); var errorSink = new ErrorSink(); var directives = visitor.Directives; diff --git a/src/Microsoft.AspNetCore.Razor.Language/HtmlCase.cs b/src/Microsoft.AspNetCore.Razor.Language/HtmlCase.cs new file mode 100644 index 0000000000..2238795334 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/HtmlCase.cs @@ -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( + "(? + /// Converts from pascal/camel case to lower kebab-case. + /// + /// + /// 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 + /// + public static string ToHtmlCase(string name) + { + return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/ITagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Razor.Language/ITagHelperDescriptorProvider.cs new file mode 100644 index 0000000000..1b061f59ba --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/ITagHelperDescriptorProvider.cs @@ -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); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/ITagHelperFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/ITagHelperFeature.cs index 9fb4b628da..1f33f948dc 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/ITagHelperFeature.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/ITagHelperFeature.cs @@ -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 GetDescriptors(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ITagHelperDescriptorResolver.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ITagHelperDescriptorResolver.cs deleted file mode 100644 index f6fca3bdcf..0000000000 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ITagHelperDescriptorResolver.cs +++ /dev/null @@ -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 -{ - /// - /// Contract used to resolve s. - /// - public interface ITagHelperDescriptorResolver - { - IEnumerable Resolve(IList errors); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorDiagnosticFactory.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorDiagnosticFactory.cs index 08cc296ccd..6177ff2d62 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorDiagnosticFactory.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorDiagnosticFactory.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorBuilder.cs index 8da0776897..6303493387 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorBuilder.cs @@ -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 InvalidNonWhitespaceAllowedChildCharacters { get; } = new HashSet( new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' }); + private string _documentation; private string _tagOutputHint; private HashSet _allowedChildTags; diff --git a/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorProviderContext.cs b/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorProviderContext.cs new file mode 100644 index 0000000000..ef228b9156 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorProviderContext.cs @@ -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 Results { get; } + + public static TagHelperDescriptorProviderContext Create() + { + return new DefaultContext(new List()); + } + + public static TagHelperDescriptorProviderContext Create(ICollection results) + { + if (results == null) + { + throw new ArgumentNullException(nameof(results)); + } + + return new DefaultContext(results); + } + + private class DefaultContext : TagHelperDescriptorProviderContext + { + public DefaultContext(ICollection results) + { + Results = results; + + Items = new DefaultItemCollection(); + } + + public override ItemCollection Items { get; } + + public override ICollection Results { get; } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolver.cs index 21bd5ae0f6..df4f40cbdb 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolver.cs @@ -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(); - VisitTagHelpers(compilation, descriptors); - VisitViewComponents(compilation, descriptors); + var providers = new ITagHelperDescriptorProvider[] + { + new DefaultTagHelperDescriptorProvider() { DesignTime = true, }, + new ViewComponentTagHelperDescriptorProvider(), + }; + + var results = new List(); + 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(); - var resolutionResult = new TagHelperResolutionResult(descriptors, diagnostics); + var resolutionResult = new TagHelperResolutionResult(results, diagnostics); return resolutionResult; } - - private void VisitTagHelpers(Compilation compilation, List results) - { - var types = new List(); - 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 results) - { - var types = new List(); - 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); - } - } - } } } \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor/CompilationTagHelperFeature.cs b/src/Microsoft.CodeAnalysis.Razor/CompilationTagHelperFeature.cs new file mode 100644 index 0000000000..bf32fd1a92 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/CompilationTagHelperFeature.cs @@ -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 GetDescriptors() + { + var results = new List(); + + 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().FirstOrDefault(); + _providers = Engine.Features.OfType().OrderBy(f => f.Order).ToArray(); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorFactory.cs b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorFactory.cs index 760d9d019d..d4dc817ab1 100644 --- a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorFactory.cs @@ -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( - "(? InvalidNonWhitespaceNameCharacters { get; } = new HashSet( - 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; } - /// - /// Converts from pascal/camel case to lower kebab-case. - /// - /// - /// 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 - /// - internal static string ToHtmlCase(string name) - { - return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant(); - } - private static string GetFullName(ITypeSymbol type) => type.ToDisplayString(FullNameTypeDisplayFormat); } } \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs new file mode 100644 index 0000000000..b9bf8f4822 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs @@ -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(); + 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); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperFeature.cs b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperFeature.cs deleted file mode 100644 index 16c8f5e7c1..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperFeature.cs +++ /dev/null @@ -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()); - } - - private class InnerResolver : ITagHelperDescriptorResolver - { - private readonly IMetadataReferenceFeature _referenceFeature; - - public InnerResolver(IMetadataReferenceFeature referenceFeature) - { - _referenceFeature = referenceFeature; - } - public IEnumerable Resolve(IList errors) - { - var compilation = CSharpCompilation.Create("__TagHelpers", references: _referenceFeature.References); - return TagHelpers.GetTagHelpers(compilation); - } - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor/TagHelperDescriptorProviderContextExtensions.cs b/src/Microsoft.CodeAnalysis.Razor/TagHelperDescriptorProviderContextExtensions.cs new file mode 100644 index 0000000000..fda4aece22 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/TagHelperDescriptorProviderContextExtensions.cs @@ -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; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor/TagHelpers.cs b/src/Microsoft.CodeAnalysis.Razor/TagHelpers.cs deleted file mode 100644 index 0c3da8e2a5..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor/TagHelpers.cs +++ /dev/null @@ -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 GetTagHelpers(Compilation compilation) - { - var results = new List(); - var errors = new ErrorSink(); - - VisitTagHelpers(compilation, results, errors); - VisitViewComponents(compilation, results, errors); - - return results; - } - - private static void VisitTagHelpers(Compilation compilation, List results, ErrorSink errors) - { - var types = new List(); - 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 results, ErrorSink errors) - { - var types = new List(); - 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); - } - } - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor/ViewComponentDiagnosticFactory.cs b/src/Microsoft.CodeAnalysis.Razor/ViewComponentDiagnosticFactory.cs new file mode 100644 index 0000000000..dacfec538a --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/ViewComponentDiagnosticFactory.cs @@ -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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperDescriptorConventions.cs b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorConventions.cs similarity index 90% rename from src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperDescriptorConventions.cs rename to src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorConventions.cs index 63bb660c65..41a0ac64f1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperDescriptorConventions.cs +++ b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorConventions.cs @@ -3,12 +3,12 @@ using Microsoft.AspNetCore.Razor.Language; -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +namespace Microsoft.CodeAnalysis.Razor { /// /// A library of methods used to generate s for view components. /// - public static class ViewComponentTagHelperDescriptorConventions + internal static class ViewComponentTagHelperDescriptorConventions { /// /// The key in a containing diff --git a/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorFactory.cs b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorFactory.cs index 52c6902afa..5202b9ca72 100644 --- a/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorFactory.cs @@ -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() + .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. + 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. + 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 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 GetInvokeMethodParameters(INamedTypeSymbol componentType) - { - var methods = componentType.GetMembers() - .OfType() - .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. - 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(); diff --git a/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorProvider.cs b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorProvider.cs new file mode 100644 index 0000000000..52e2dedc39 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTagHelperDescriptorProvider.cs @@ -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(); + 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); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor/ViewComponentTypeVisitor.cs b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTypeVisitor.cs index e689daca6a..8dda01d30b 100644 --- a/src/Microsoft.CodeAnalysis.Razor/ViewComponentTypeVisitor.cs +++ b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTypeVisitor.cs @@ -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 _results; + private readonly INamedTypeSymbol _viewComponentAttribute; + private readonly INamedTypeSymbol _nonViewComponentAttribute; + private readonly List _results; public static ViewComponentTypeVisitor Create(Compilation compilation, List 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; } diff --git a/src/Microsoft.CodeAnalysis.Razor/ViewComponentTypes.cs b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTypes.cs index f7f7a560b3..b48dad9c41 100644 --- a/src/Microsoft.CodeAnalysis.Razor/ViewComponentTypes.cs +++ b/src/Microsoft.CodeAnalysis.Razor/ViewComponentTypes.cs @@ -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"; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs index 8699149f6c..de2293f16b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs @@ -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()); } }); } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ModelExpressionPassTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ModelExpressionPassTest.cs index e0e9673beb..319212236f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ModelExpressionPassTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ModelExpressionPassTest.cs @@ -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 Resolve(IList errors) - { - return TagHelpers; - } - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperDescriptorConventionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperDescriptorConventionsTest.cs index f0bd076179..fa6de930b7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperDescriptorConventionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperDescriptorConventionsTest.cs @@ -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 diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperPassTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperPassTest.cs index 93dbbf5312..0964c8b381 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperPassTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperPassTest.cs @@ -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 Resolve(IList errors) - { - return TagHelpers; - } - } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorTagHelperBinderPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorTagHelperBinderPhaseTest.cs index 2969187cd8..557d390b0a 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorTagHelperBinderPhaseTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorTagHelperBinderPhaseTest.cs @@ -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()); }); 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(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(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 Resolve(IList errors) - { - errors.Add(_error); - - return new[] { CreateTagHelperDescriptor( - tagName: _tagName, - typeName: null, - assemblyName: "TestAssembly") }; - } - } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/HtmlCaseTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/HtmlCaseTest.cs new file mode 100644 index 0000000000..fcba8dbdc1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/HtmlCaseTest.cs @@ -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 + { + { "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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Langauge/TestTagHelperDescriptorResolver.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Langauge/TestTagHelperDescriptorResolver.cs deleted file mode 100644 index c868483ef7..0000000000 --- a/test/Microsoft.AspNetCore.Razor.Test.Common/Langauge/TestTagHelperDescriptorResolver.cs +++ /dev/null @@ -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 tagHelpers) - { - TagHelpers.AddRange(tagHelpers); - } - - public List TagHelpers { get; } = new List(); - - public IEnumerable Resolve(IList errors) - { - return TagHelpers; - } - } -} diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Langauge/TestTagHelperFeature.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Langauge/TestTagHelperFeature.cs index 682fd06d73..ad4039007e 100644 --- a/test/Microsoft.AspNetCore.Razor.Test.Common/Langauge/TestTagHelperFeature.cs +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Langauge/TestTagHelperFeature.cs @@ -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(); } public TestTagHelperFeature(IEnumerable tagHelpers) { - Resolver = new TestTagHelperDescriptorResolver(tagHelpers); + TagHelpers = new List(tagHelpers); } - public List TagHelpers => ((TestTagHelperDescriptorResolver)Resolver).TagHelpers; + public List TagHelpers { get; } - public ITagHelperDescriptorResolver Resolver { get; } + public IReadOnlyList GetDescriptors() + { + return TagHelpers.ToArray(); + } } } diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorFactoryTest.cs b/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorFactoryTest.cs index 5772d3c55b..afded7d9e9 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorFactoryTest.cs @@ -1970,35 +1970,6 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces Assert.Equal(expectedDiagnostics, descriptor.GetAllDiagnostics()); } - public static TheoryData HtmlConversionData - { - get - { - return new TheoryData - { - { "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 diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/TestCompilation.cs b/test/Microsoft.CodeAnalysis.Razor.Test/TestCompilation.cs index 01887eb697..ab5869c6cb 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Test/TestCompilation.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Test/TestCompilation.cs @@ -14,6 +14,29 @@ namespace Microsoft.CodeAnalysis.Razor { public static class TestCompilation { + private static IEnumerable _metadataReferences; + + public static IEnumerable 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 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); diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/ViewComponentTagHelperDescriptorFactoryTest.cs b/test/Microsoft.CodeAnalysis.Razor.Test/ViewComponentTagHelperDescriptorFactoryTest.cs index 35a39901a2..075a25e9bf 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Test/ViewComponentTagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Test/ViewComponentTagHelperDescriptorFactoryTest.cs @@ -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 Foo, Dictionary Bar) => null; } + + public class ViewComponentWithoutInvokeMethod + { + } + + public class AsyncViewComponentWithGenericTask + { + public Task 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 Invoke() => null; + } } \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/ViewComponentTagHelperDescriptorProviderTest.cs b/test/Microsoft.CodeAnalysis.Razor.Test/ViewComponentTagHelperDescriptorProviderTest.cs new file mode 100644 index 0000000000..9179783cc3 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Test/ViewComponentTagHelperDescriptorProviderTest.cs @@ -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); + } + } +}