Skip to content

Commit

Permalink
Fixes: #9281 - implement SignalR detection
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Ryan Nowak authored and rynowak committed May 8, 2019
1 parent b383695 commit 5d71ea5
Show file tree
Hide file tree
Showing 14 changed files with 501 additions and 2 deletions.
59 changes: 59 additions & 0 deletions src/Analyzers/Analyzers/src/CompilationFeatureDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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();
}

}
}
61 changes: 61 additions & 0 deletions src/Analyzers/Analyzers/src/ConfigureMethodVisitor.cs
Original file line number Diff line number Diff line change
@@ -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._types;
}

private readonly StartupSymbols _symbols;
private readonly List<IMethodSymbol> _types;

private ConfigureMethodVisitor(StartupSymbols symbols)
{
_symbols = symbols;
_types = 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))
{
_types.Add(symbol);
}
}
}
}
25 changes: 25 additions & 0 deletions src/Analyzers/Analyzers/src/StartupFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,30 @@ public static bool IsConfigure(StartupSymbols symbols, IMethodSymbol symbol)

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;
}
}
}
23 changes: 21 additions & 2 deletions src/Analyzers/Analyzers/src/SymbolNames.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,5 +20,26 @@ public static class IServiceCollection
{
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";
}
}
}
10 changes: 10 additions & 0 deletions src/Analyzers/Analyzers/src/WellKnownFeatures.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
51 changes: 51 additions & 0 deletions src/Analyzers/Analyzers/test/CompilationFeatureDetectorTest.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
41 changes: 41 additions & 0 deletions src/Analyzers/Analyzers/test/ConfigureMethodVisitorTest.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
});
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
}
}
Original file line number Diff line number Diff line change
@@ -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");
});
}
}
}
Original file line number Diff line number Diff line change
@@ -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 =>
{
});
}
}
}
Loading

0 comments on commit 5d71ea5

Please sign in to comment.