diff --git a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs index b0d07db459..3b7c410b0d 100644 --- a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs +++ b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs @@ -31,8 +31,19 @@ namespace Microsoft.CodeAnalysis.Razor return; } + var @interface = compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper); + var @string = compilation.GetSpecialType(SpecialType.System_String); + + // Ensure ITagHelper and System.String are available. They may be missing or + // errored if we're missing references. + if (@interface == null || @interface.TypeKind == TypeKind.Error || + @string == null || @string.TypeKind == TypeKind.Error) + { + return; + } + var types = new List(); - var visitor = TagHelperTypeVisitor.Create(compilation, types); + var visitor = new TagHelperTypeVisitor(@interface, types); // We always visit the global namespace. visitor.Visit(compilation.Assembly.GlobalNamespace); diff --git a/src/Microsoft.CodeAnalysis.Razor/TagHelperTypeVisitor.cs b/src/Microsoft.CodeAnalysis.Razor/TagHelperTypeVisitor.cs index 625e8284c1..dfe5a9f0a1 100644 --- a/src/Microsoft.CodeAnalysis.Razor/TagHelperTypeVisitor.cs +++ b/src/Microsoft.CodeAnalysis.Razor/TagHelperTypeVisitor.cs @@ -8,14 +8,8 @@ namespace Microsoft.CodeAnalysis.Razor // Visits top-level types and finds interface implementations. internal class TagHelperTypeVisitor : SymbolVisitor { - private INamedTypeSymbol _interface; - private List _results; - - public static TagHelperTypeVisitor Create(Compilation compilation, List results) - { - var @interface = compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper); - return new TagHelperTypeVisitor(@interface, results); - } + private readonly INamedTypeSymbol _interface; + private readonly List _results; public TagHelperTypeVisitor(INamedTypeSymbol @interface, List results) { @@ -41,12 +35,8 @@ namespace Microsoft.CodeAnalysis.Razor internal bool IsTagHelper(INamedTypeSymbol symbol) { - if (_interface == null) - { - return false; - } - return + symbol.TypeKind != TypeKind.Error && symbol.DeclaredAccessibility == Accessibility.Public && !symbol.IsAbstract && !symbol.IsGenericType && diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorProviderTest.cs b/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorProviderTest.cs index b7cbf6a9f6..04883a123a 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorProviderTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorProviderTest.cs @@ -1,9 +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 System.IO; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.CodeAnalysis.CSharp; using Xunit; namespace Microsoft.CodeAnalysis.Razor @@ -21,6 +24,7 @@ namespace Microsoft.CodeAnalysis.Razor var descriptorProvider = new DefaultTagHelperDescriptorProvider(); var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); context.ExcludeHidden = true; // Act @@ -33,5 +37,93 @@ namespace Microsoft.CodeAnalysis.Razor var editorBrowsableDescriptor = context.Results.Where(descriptor => descriptor.GetTypeName() == editorBrowsableTypeName); Assert.Empty(editorBrowsableDescriptor); } + + [Fact] + public void Execute_NoOpsIfCompilationSymbolIsNotSet() + { + // Arrange + var descriptorProvider = new DefaultTagHelperDescriptorProvider(); + + var context = TagHelperDescriptorProviderContext.Create(); + + // Act + descriptorProvider.Execute(context); + + // Assert + Assert.Empty(context.Results); + } + + [Fact] + public void Execute_NoOpsIfTagHelperInterfaceCannotBeFound() + { + // Arrange + var references = new[] + { + MetadataReference.CreateFromFile(typeof(string).Assembly.Location), + }; + var compilation = CSharpCompilation.Create("Test", references: references); + + var descriptorProvider = new DefaultTagHelperDescriptorProvider(); + + var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); + + // Act + descriptorProvider.Execute(context); + + // Assert + Assert.Empty(context.Results); + } + + [Fact] + public void Execute_NoOpsIfStringCannotBeFound() + { + // Arrange + var references = new[] + { + MetadataReference.CreateFromFile(typeof(ITagHelper).Assembly.Location), + }; + var compilation = CSharpCompilation.Create("Test", references: references); + + var descriptorProvider = new DefaultTagHelperDescriptorProvider(); + + var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); + + // Act + descriptorProvider.Execute(context); + + // Assert + Assert.Empty(context.Results); + } + + [Fact] + public void Execute_DiscoversTagHelpersFromCompilation() + { + // Arrange + var references = new[] + { + MetadataReference.CreateFromFile(typeof(string).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ITagHelper).Assembly.Location), + }; + var projectDirectory = TestProject.GetProjectDirectory(GetType()); + var tagHelperContent = File.ReadAllText(Path.Combine(projectDirectory, "TagHelperTypes.cs")); + var syntaxTree = CSharpSyntaxTree.ParseText(tagHelperContent); + var compilation = CSharpCompilation.Create("Test", references: references, syntaxTrees: new[] { syntaxTree }); + + var descriptorProvider = new DefaultTagHelperDescriptorProvider(); + + var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); + + // Act + descriptorProvider.Execute(context); + + // Assert + Assert.Collection( + context.Results.OrderBy(r => r.Name), + tagHelper => Assert.Equal(typeof(Valid_InheritedTagHelper).FullName, tagHelper.Name), + tagHelper => Assert.Equal(typeof(Valid_PlainTagHelper).FullName, tagHelper.Name)); + } } } diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypeVisitorTest.cs b/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypeVisitorTest.cs index 8f14017c91..88e26016e5 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypeVisitorTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypeVisitorTest.cs @@ -98,24 +98,4 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces public string Invoke(string foo) => null; } } - - public abstract class Invalid_AbstractTagHelper : TagHelper - { - } - - public class Invalid_GenericTagHelper : TagHelper - { - } - - internal class Invalid_InternalTagHelper : TagHelper - { - } - - public class Valid_PlainTagHelper : TagHelper - { - } - - public class Valid_InheritedTagHelper : Valid_PlainTagHelper - { - } } diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypes.cs b/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypes.cs new file mode 100644 index 0000000000..fea9cd5ad3 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypes.cs @@ -0,0 +1,27 @@ +// 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.TagHelpers; + +namespace Microsoft.CodeAnalysis.Razor +{ + public abstract class Invalid_AbstractTagHelper : TagHelper + { + } + + public class Invalid_GenericTagHelper : TagHelper + { + } + + internal class Invalid_InternalTagHelper : TagHelper + { + } + + public class Valid_PlainTagHelper : TagHelper + { + } + + public class Valid_InheritedTagHelper : Valid_PlainTagHelper + { + } +}