This change adds detection of various SignalR configure-related calls to the startup analysis infrastructure. Also adds a shim that VS is going to call into to analyze the project pre-publish.
This commit is contained in:
parent
4db8260c6c
commit
7642f9d12a
|
|
@ -0,0 +1,58 @@
|
|||
// 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.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers
|
||||
{
|
||||
internal static class CompilationFeatureDetector
|
||||
{
|
||||
public static async Task<IImmutableSet<string>> DetectFeaturesAsync(
|
||||
Compilation compilation,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var symbols = new StartupSymbols(compilation);
|
||||
if (!symbols.HasRequiredSymbols)
|
||||
{
|
||||
// Cannot find ASP.NET Core types.
|
||||
return ImmutableHashSet<string>.Empty;
|
||||
}
|
||||
|
||||
var features = ImmutableHashSet.CreateBuilder<string>();
|
||||
|
||||
// Find configure methods in the project's assembly
|
||||
var configureMethods = ConfigureMethodVisitor.FindConfigureMethods(symbols, compilation.Assembly);
|
||||
for (var i = 0; i < configureMethods.Count; i++)
|
||||
{
|
||||
var configureMethod = configureMethods[i];
|
||||
|
||||
// Handles the case where a method is using partial definitions. We don't expect this to occur, but still handle it correctly.
|
||||
var syntaxReferences = configureMethod.DeclaringSyntaxReferences;
|
||||
for (var j = 0; j < syntaxReferences.Length; j++)
|
||||
{
|
||||
var semanticModel = compilation.GetSemanticModel(syntaxReferences[j].SyntaxTree);
|
||||
|
||||
var syntax = await syntaxReferences[j].GetSyntaxAsync(cancellationToken).ConfigureAwait(false);
|
||||
var operation = semanticModel.GetOperation(syntax);
|
||||
|
||||
// Look for a call to one of the SignalR gestures that applies to the Configure method.
|
||||
if (operation
|
||||
.Descendants()
|
||||
.OfType<IInvocationOperation>()
|
||||
.Any(op => StartupFacts.IsSignalRConfigureMethodGesture(op.TargetMethod)))
|
||||
{
|
||||
features.Add(WellKnownFeatures.SignalR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return features.ToImmutable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// 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.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers
|
||||
{
|
||||
internal class ConfigureMethodVisitor : SymbolVisitor
|
||||
{
|
||||
public static List<IMethodSymbol> FindConfigureMethods(StartupSymbols symbols, IAssemblySymbol assembly)
|
||||
{
|
||||
var visitor = new ConfigureMethodVisitor(symbols);
|
||||
visitor.Visit(assembly);
|
||||
return visitor._methods;
|
||||
}
|
||||
|
||||
private readonly StartupSymbols _symbols;
|
||||
private readonly List<IMethodSymbol> _methods;
|
||||
|
||||
private ConfigureMethodVisitor(StartupSymbols symbols)
|
||||
{
|
||||
_symbols = symbols;
|
||||
_methods = new List<IMethodSymbol>();
|
||||
}
|
||||
|
||||
public override void VisitAssembly(IAssemblySymbol symbol)
|
||||
{
|
||||
Visit(symbol.GlobalNamespace);
|
||||
}
|
||||
|
||||
public override void VisitNamespace(INamespaceSymbol symbol)
|
||||
{
|
||||
foreach (var type in symbol.GetTypeMembers())
|
||||
{
|
||||
Visit(type);
|
||||
}
|
||||
|
||||
foreach (var @namespace in symbol.GetNamespaceMembers())
|
||||
{
|
||||
Visit(@namespace);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitNamedType(INamedTypeSymbol symbol)
|
||||
{
|
||||
foreach (var member in symbol.GetMembers())
|
||||
{
|
||||
Visit(member);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitMethod(IMethodSymbol symbol)
|
||||
{
|
||||
if (StartupFacts.IsConfigure(_symbols, symbol))
|
||||
{
|
||||
_methods.Add(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,5 +122,30 @@ namespace Microsoft.AspNetCore.Analyzers
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Based on the three known gestures for including SignalR in a ConfigureMethod:
|
||||
// UseSignalR() // middleware
|
||||
// MapHub<>() // endpoint routing
|
||||
// MapBlazorHub() // server-side blazor
|
||||
//
|
||||
// To be slightly less brittle, we don't look at the exact symbols and instead just look
|
||||
// at method names in here. We're NOT worried about false negatives, because all of these
|
||||
// cases contain words like SignalR or Hub.
|
||||
public static bool IsSignalRConfigureMethodGesture(IMethodSymbol symbol)
|
||||
{
|
||||
if (symbol == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(symbol));
|
||||
}
|
||||
|
||||
if (string.Equals(symbol.Name, SymbolNames.SignalRAppBuilderExtensions.UseSignalRMethodName, StringComparison.Ordinal) ||
|
||||
string.Equals(symbol.Name, SymbolNames.HubEndpointRouteBuilderExtensions.MapHubMethodName, StringComparison.Ordinal) ||
|
||||
string.Equals(symbol.Name, SymbolNames.ComponentEndpointRouteBuilderExtensions.MapBlazorHubMethodName, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers
|
||||
{
|
||||
internal static class SymbolNames
|
||||
|
|
@ -22,5 +20,26 @@ namespace Microsoft.AspNetCore.Analyzers
|
|||
{
|
||||
public const string MetadataName = "Microsoft.Extensions.DependencyInjection.IServiceCollection";
|
||||
}
|
||||
|
||||
public static class ComponentEndpointRouteBuilderExtensions
|
||||
{
|
||||
public const string MetadataName = "Microsoft.AspNetCore.Builder.ComponentEndpointRouteBuilderExtensions";
|
||||
|
||||
public const string MapBlazorHubMethodName = "MapBlazorHub";
|
||||
}
|
||||
|
||||
public static class HubEndpointRouteBuilderExtensions
|
||||
{
|
||||
public const string MetadataName = "Microsoft.AspNetCore.Builder.HubEndpointRouteBuilderExtensions";
|
||||
|
||||
public const string MapHubMethodName = "MapHub";
|
||||
}
|
||||
|
||||
public static class SignalRAppBuilderExtensions
|
||||
{
|
||||
public const string MetadataName = "Microsoft.AspNetCore.Builder.SignalRAppBuilderExtensions";
|
||||
|
||||
public const string UseSignalRMethodName = "UseSignalR";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers
|
||||
{
|
||||
internal static class WellKnownFeatures
|
||||
{
|
||||
public static readonly string SignalR = nameof(SignalR);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// 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 System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Analyzers.TestFiles.CompilationFeatureDetectorTest;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers
|
||||
{
|
||||
public class CompilationFeatureDetectorTest : AnalyzerTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task DetectFeaturesAsync_FindsNoFeatures()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await CreateCompilationAsync(nameof(StartupWithNoFeatures));
|
||||
var symbols = new StartupSymbols(compilation);
|
||||
|
||||
var type = (INamedTypeSymbol)compilation.GetSymbolsWithName(nameof(StartupWithNoFeatures)).Single();
|
||||
Assert.True(StartupFacts.IsStartupClass(symbols, type));
|
||||
|
||||
// Act
|
||||
var features = await CompilationFeatureDetector.DetectFeaturesAsync(compilation);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(features);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(StartupWithUseSignalR))]
|
||||
[InlineData(nameof(StartupWithMapHub))]
|
||||
[InlineData(nameof(StartupWithMapBlazorHub))]
|
||||
public async Task DetectFeaturesAsync_FindsSignalR(string source)
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await CreateCompilationAsync(source);
|
||||
var symbols = new StartupSymbols(compilation);
|
||||
|
||||
var type = (INamedTypeSymbol)compilation.GetSymbolsWithName(source).Single();
|
||||
Assert.True(StartupFacts.IsStartupClass(symbols, type));
|
||||
|
||||
// Act
|
||||
var features = await CompilationFeatureDetector.DetectFeaturesAsync(compilation);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(features, f => Assert.Equal(WellKnownFeatures.SignalR, f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// 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 System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers
|
||||
{
|
||||
public class ConfigureMethodVisitorTest : AnalyzerTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task FindConfigureMethods_AtDifferentScopes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new string[]
|
||||
{
|
||||
"global::ANamespace.Nested.Startup.Configure",
|
||||
"global::ANamespace.Nested.Startup.NestedStartup.Configure",
|
||||
"global::ANamespace.Startup.ConfigureDevelopment",
|
||||
"global::ANamespace.Startup.NestedStartup.ConfigureTest",
|
||||
"global::Another.AnotherStartup.Configure",
|
||||
"global::GlobalStartup.Configure",
|
||||
};
|
||||
|
||||
var compilation = await CreateCompilationAsync("Startup");
|
||||
var symbols = new StartupSymbols(compilation);
|
||||
|
||||
// Act
|
||||
var results = ConfigureMethodVisitor.FindConfigureMethods(symbols, compilation.Assembly);
|
||||
|
||||
// Assert
|
||||
var actual = results
|
||||
.Select(m => m.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + "." + m.Name)
|
||||
.OrderBy(s => s)
|
||||
.ToArray();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// 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.Builder;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers.TestFiles.CompilationFeatureDetectorTest
|
||||
{
|
||||
public class StartupWithMapBlazorHub
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapBlazorHub();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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.Builder;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers.TestFiles.CompilationFeatureDetectorTest
|
||||
{
|
||||
public class StartupWithMapHub
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapHub<MyHub>("/test");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class MyHub : Hub
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// 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.Builder;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers.TestFiles.CompilationFeatureDetectorTest
|
||||
{
|
||||
public class StartupWithNoFeatures
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToFile("index.html");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// 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.Builder;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers.TestFiles.CompilationFeatureDetectorTest
|
||||
{
|
||||
public class StartupWithUseSignalR
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseSignalR(routes =>
|
||||
{
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// 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.Builder;
|
||||
|
||||
public class GlobalStartup
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace Another
|
||||
{
|
||||
public class AnotherStartup
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace ANamespace
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public void ConfigureDevelopment(IApplicationBuilder app)
|
||||
{
|
||||
}
|
||||
|
||||
public class NestedStartup
|
||||
{
|
||||
public void ConfigureTest(IApplicationBuilder app)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace ANamespace.Nested
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
}
|
||||
|
||||
public class NestedStartup
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,16 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../../Analyzers/src/CompilationFeatureDetector.cs">
|
||||
<Link>%(FileName)%(Extension)</Link>
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>$(ContentTargetFolders)\cs\netstandard1.0\shared\</PackagePath>
|
||||
</Compile>
|
||||
<Compile Include="../../Analyzers/src/ConfigureMethodVisitor.cs">
|
||||
<Link>%(FileName)%(Extension)</Link>
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>$(ContentTargetFolders)\cs\netstandard1.0\shared\</PackagePath>
|
||||
</Compile>
|
||||
<Compile Include="../../Analyzers/src/StartupFacts.cs">
|
||||
<Link>%(FileName)%(Extension)</Link>
|
||||
<Pack>true</Pack>
|
||||
|
|
@ -43,6 +53,11 @@
|
|||
<Pack>true</Pack>
|
||||
<PackagePath>$(ContentTargetFolders)\cs\netstandard1.0\shared\</PackagePath>
|
||||
</Compile>
|
||||
<Compile Include="../../Analyzers/src/WellKnownFeatures.cs">
|
||||
<Link>%(FileName)%(Extension)</Link>
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>$(ContentTargetFolders)\cs\netstandard1.0\shared\</PackagePath>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
// 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.Immutable;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.VisualStudio.LanguageServices;
|
||||
using Microsoft.VisualStudio.Shell;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers.FeatureDetection
|
||||
{
|
||||
// Be very careful making changes to this file. No project in this repo builds it.
|
||||
//
|
||||
// If you need to verify a change, make a local project (net472) and copy in everything included by this project.
|
||||
//
|
||||
// You'll also need some nuget packages like:
|
||||
// - Microsoft.VisualStudio.LanguageServices
|
||||
// - Microsoft.VisualStudio.Shell.15.0
|
||||
// - Microsoft.VisualStudio.Threading
|
||||
[Export(typeof(ProjectCompilationFeatureDetector))]
|
||||
internal class ProjectCompilationFeatureDetector
|
||||
{
|
||||
private readonly Lazy<VisualStudioWorkspace> _workspace;
|
||||
|
||||
[ImportingConstructor]
|
||||
public ProjectCompilationFeatureDetector(Lazy<VisualStudioWorkspace> workspace)
|
||||
{
|
||||
_workspace = workspace;
|
||||
}
|
||||
|
||||
public async Task<IImmutableSet<string>> DetectFeaturesAsync(string projectFullPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (projectFullPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(projectFullPath));
|
||||
}
|
||||
|
||||
// If the workspace is uninitialized, we need to do the first access on the UI thread.
|
||||
//
|
||||
// This is very unlikely to occur, but doing it here for completeness.
|
||||
if (!_workspace.IsValueCreated)
|
||||
{
|
||||
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
|
||||
GC.KeepAlive(_workspace.Value);
|
||||
await TaskScheduler.Default;
|
||||
}
|
||||
|
||||
var workspace = _workspace.Value;
|
||||
var solution = workspace.CurrentSolution;
|
||||
|
||||
var project = GetProject(solution, projectFullPath);
|
||||
if (project == null)
|
||||
{
|
||||
// Cannot find matching project.
|
||||
return ImmutableHashSet<string>.Empty;
|
||||
}
|
||||
|
||||
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await CompilationFeatureDetector.DetectFeaturesAsync(compilation, cancellationToken);
|
||||
}
|
||||
|
||||
private static Project GetProject(Solution solution, string projectFilePath)
|
||||
{
|
||||
foreach (var project in solution.Projects)
|
||||
{
|
||||
if (string.Equals(projectFilePath, project.FilePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return project;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue