diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs index 847e22a674..4316f6ee3b 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs @@ -45,7 +45,10 @@ namespace Microsoft.CodeAnalysis.Razor context.IncludeDocumentation = true; var compilation = await project.WorkspaceProject.GetCompilationAsync().ConfigureAwait(false); - context.SetCompilation(compilation); + if (CompilationTagHelperFeature.IsValidCompilation(compilation)) + { + context.SetCompilation(compilation); + } for (var i = 0; i < providers.Length; i++) { diff --git a/src/Microsoft.CodeAnalysis.Razor/CompilationTagHelperFeature.cs b/src/Microsoft.CodeAnalysis.Razor/CompilationTagHelperFeature.cs index 6aa81ff506..2b343f74e2 100644 --- a/src/Microsoft.CodeAnalysis.Razor/CompilationTagHelperFeature.cs +++ b/src/Microsoft.CodeAnalysis.Razor/CompilationTagHelperFeature.cs @@ -19,7 +19,10 @@ namespace Microsoft.CodeAnalysis.Razor var context = TagHelperDescriptorProviderContext.Create(results); var compilation = CSharpCompilation.Create("__TagHelpers", references: _referenceFeature.References); - context.SetCompilation(compilation); + if (IsValidCompilation(compilation)) + { + context.SetCompilation(compilation); + } for (var i = 0; i < _providers.Length; i++) { @@ -34,5 +37,18 @@ namespace Microsoft.CodeAnalysis.Razor _referenceFeature = Engine.Features.OfType().FirstOrDefault(); _providers = Engine.Features.OfType().OrderBy(f => f.Order).ToArray(); } + + internal static bool IsValidCompilation(Compilation compilation) + { + var iTagHelper = compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper); + var @string = compilation.GetSpecialType(SpecialType.System_String); + + // Do some minimal tests to verify the compilation is valid. If symbols for ITagHelper or System.String + // are missing or errored, the compilation may be missing references. + return iTagHelper != null && + iTagHelper.TypeKind != TypeKind.Error && + @string != null && + @string.TypeKind != TypeKind.Error; + } } } diff --git a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs index 3b7c410b0d..b0d07db459 100644 --- a/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs +++ b/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorProvider.cs @@ -31,19 +31,8 @@ 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 = new TagHelperTypeVisitor(@interface, types); + var visitor = TagHelperTypeVisitor.Create(compilation, 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 dfe5a9f0a1..aa240a1eac 100644 --- a/src/Microsoft.CodeAnalysis.Razor/TagHelperTypeVisitor.cs +++ b/src/Microsoft.CodeAnalysis.Razor/TagHelperTypeVisitor.cs @@ -8,8 +8,14 @@ namespace Microsoft.CodeAnalysis.Razor // Visits top-level types and finds interface implementations. internal class TagHelperTypeVisitor : SymbolVisitor { - private readonly INamedTypeSymbol _interface; - private readonly List _results; + 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); + } public TagHelperTypeVisitor(INamedTypeSymbol @interface, List results) { @@ -35,6 +41,11 @@ namespace Microsoft.CodeAnalysis.Razor internal bool IsTagHelper(INamedTypeSymbol symbol) { + if (_interface == null) + { + return false; + } + return symbol.TypeKind != TypeKind.Error && symbol.DeclaredAccessibility == Accessibility.Public && diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/CompilationTagHelperFeatureTest.cs b/test/Microsoft.CodeAnalysis.Razor.Test/CompilationTagHelperFeatureTest.cs new file mode 100644 index 0000000000..e545d671e2 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Test/CompilationTagHelperFeatureTest.cs @@ -0,0 +1,131 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.CodeAnalysis.CSharp; +using Moq; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor +{ + public class CompilationTagHelperFeatureTest + { + [Fact] + public void IsValidCompilation_ReturnsFalseIfITagHelperInterfaceCannotBeFound() + { + // Arrange + var references = new[] + { + MetadataReference.CreateFromFile(typeof(string).Assembly.Location), + }; + var compilation = CSharpCompilation.Create("Test", references: references); + + // Act + var result = CompilationTagHelperFeature.IsValidCompilation(compilation); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsValidCompilation_ReturnsFalseIfSystemStringCannotBeFound() + { + // Arrange + var references = new[] + { + MetadataReference.CreateFromFile(typeof(ITagHelper).Assembly.Location), + }; + var compilation = CSharpCompilation.Create("Test", references: references); + + // Act + var result = CompilationTagHelperFeature.IsValidCompilation(compilation); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsValidCompilation_ReturnsTrueIfWellKnownTypesAreFound() + { + // Arrange + var references = new[] + { + MetadataReference.CreateFromFile(typeof(string).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ITagHelper).Assembly.Location), + }; + var compilation = CSharpCompilation.Create("Test", references: references); + + // Act + var result = CompilationTagHelperFeature.IsValidCompilation(compilation); + + // Assert + Assert.True(result); + } + + [Fact] + public void GetDescriptors_DoesNotSetCompilation_IfCompilationIsInvalid() + { + // Arrange + Compilation compilation = null; + var provider = new Mock(); + provider.Setup(c => c.Execute(It.IsAny())) + .Callback(c => compilation = c.GetCompilation()) + .Verifiable(); + + var engine = RazorProjectEngine.Create( + configure => + { + configure.Features.Add(new DefaultMetadataReferenceFeature()); + configure.Features.Add(provider.Object); + configure.Features.Add(new CompilationTagHelperFeature()); + }); + + var feature = engine.EngineFeatures.OfType().First(); + + // Act + var result = feature.GetDescriptors(); + + // Assert + Assert.Empty(result); + provider.Verify(); + Assert.Null(compilation); + } + + [Fact] + public void GetDescriptors_SetsCompilation_IfCompilationIsValid() + { + // Arrange + Compilation compilation = null; + var provider = new Mock(); + provider.Setup(c => c.Execute(It.IsAny())) + .Callback(c => compilation = c.GetCompilation()) + .Verifiable(); + + var references = new[] + { + MetadataReference.CreateFromFile(typeof(string).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ITagHelper).Assembly.Location), + }; + + var engine = RazorProjectEngine.Create( + configure => + { + configure.Features.Add(new DefaultMetadataReferenceFeature { References = references }); + configure.Features.Add(provider.Object); + configure.Features.Add(new CompilationTagHelperFeature()); + }); + + var feature = engine.EngineFeatures.OfType().First(); + + // Act + var result = feature.GetDescriptors(); + + // Assert + Assert.Empty(result); + provider.Verify(); + Assert.NotNull(compilation); + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorProviderTest.cs b/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorProviderTest.cs index 04883a123a..0a933ed34f 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorProviderTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Test/DefaultTagHelperDescriptorProviderTest.cs @@ -1,11 +1,9 @@ // 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; @@ -39,7 +37,7 @@ namespace Microsoft.CodeAnalysis.Razor } [Fact] - public void Execute_NoOpsIfCompilationSymbolIsNotSet() + public void Execute_NoOpsIfCompilationIsNotSet() { // Arrange var descriptorProvider = new DefaultTagHelperDescriptorProvider(); @@ -52,78 +50,5 @@ namespace Microsoft.CodeAnalysis.Razor // 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/Microsoft.CodeAnalysis.Razor.Test.csproj b/test/Microsoft.CodeAnalysis.Razor.Test/Microsoft.CodeAnalysis.Razor.Test.csproj index 7b4cde7c14..d95d005405 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Test/Microsoft.CodeAnalysis.Razor.Test.csproj +++ b/test/Microsoft.CodeAnalysis.Razor.Test/Microsoft.CodeAnalysis.Razor.Test.csproj @@ -23,6 +23,7 @@ + diff --git a/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypeVisitorTest.cs b/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypeVisitorTest.cs index 88e26016e5..77a85be01a 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypeVisitorTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypeVisitorTest.cs @@ -86,9 +86,6 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces Assert.False(isTagHelper); } - - - public class Invalid_NestedPublicTagHelper : TagHelper { } @@ -98,4 +95,24 @@ 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 deleted file mode 100644 index fea9cd5ad3..0000000000 --- a/test/Microsoft.CodeAnalysis.Razor.Test/TagHelperTypes.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 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 - { - } -}