예제 #1
0
        public static bool IsConfigureServices(StartupSymbols symbols, IMethodSymbol symbol)
        {
            if (symbol == null)
            {
                throw new ArgumentNullException(nameof(symbol));
            }

            if (symbol.DeclaredAccessibility != Accessibility.Public)
            {
                return(false);
            }

            if (!string.Equals(symbol.Name, "ConfigureServices", StringComparison.Ordinal))
            {
                return(false);
            }

            if (symbol.Parameters.Length != 1)
            {
                return(false);
            }

            if (symbol.Parameters[0].Type != symbols.IServiceCollection)
            {
                return(false);
            }

            return(true);
        }
예제 #2
0
        private void OnCompilationStart(CompilationStartAnalysisContext context)
        {
            var symbols = new StartupSymbols(context.Compilation);

            // Don't run analyzer if ASP.NET Core types cannot be found
            if (symbols.IServiceCollection == null || symbols.IApplicationBuilder == null)
            {
                return;
            }

            // This analyzer is a general-purpose framework that functions by:
            // 1. Discovering Startup methods (ConfigureServices, Configure)
            // 2. Launching additional analyses of these Startup methods and collecting results
            // 3. Running a final pass to add diagnostics based on computed state
            var analyses = new ConcurrentBag <StartupComputedAnalysis>();

            context.RegisterOperationBlockStartAction(context =>
            {
                AnalyzeStartupMethods(context, symbols, analyses);
            });

            // Run after analyses have had a chance to finish to add diagnostics.
            context.RegisterCompilationEndAction(analysisContext =>
            {
                RunAnalysis(analysisContext, analyses);
            });
        }
예제 #3
0
        public static bool IsStartupClass(StartupSymbols symbols, INamedTypeSymbol type)
        {
            if (symbols == null)
            {
                throw new ArgumentNullException(nameof(symbols));
            }

            if (type == null)
            {
                throw new ArgumentNullException(nameof(type));
            }

            // It's not good enough to just look for a method called ConfigureServices or Configure as a hueristic.
            // ConfigureServices might not appear in trivial cases, and Configure might be named ConfigureDevelopment
            // or something similar.
            //
            // Additionally, a startup class could be called anything and wired up explicitly.
            //
            // Since we already are analyzing the symbol it should be cheap to do a pass over the members.
            var members = type.GetMembers();

            for (var i = 0; i < members.Length; i++)
            {
                if (members[i] is IMethodSymbol method && (IsConfigureServices(symbols, method) || IsConfigure(symbols, method)))
                {
                    return(true);
                }
            }

            return(false);
        }
예제 #4
0
 public StartupAnalysis(
     StartupSymbols startupSymbols,
     ImmutableDictionary <INamedTypeSymbol, ImmutableArray <object> > analysesByType)
 {
     StartupSymbols  = startupSymbols;
     _analysesByType = analysesByType;
 }
예제 #5
0
        // Based on StartupLoader. The philosophy is that we want to do analysis only on things
        // that would be recognized as a ConfigureServices method to avoid false positives.
        //
        // The ConfigureServices method follows the naming pattern `Configure{Environment?}Services` (ignoring case).
        // The ConfigureServices method must be public.
        // The ConfigureServices method can be instance or static.
        // The ConfigureServices method cannot have other parameters besides IServiceCollection.
        //
        // The ConfigureServices method does not actually need to accept IServiceCollection
        // but we exclude that case because a ConfigureServices method that doesn't accept an
        // IServiceCollection can't do anything interesting to analysis.
        public static bool IsConfigureServices(StartupSymbols symbols, IMethodSymbol symbol)
        {
            if (symbol == null)
            {
                throw new ArgumentNullException(nameof(symbol));
            }

            if (symbol.DeclaredAccessibility != Accessibility.Public)
            {
                return(false);
            }

            if (symbol.Name == null ||
                !symbol.Name.StartsWith(SymbolNames.ConfigureServicesMethodPrefix, StringComparison.OrdinalIgnoreCase) ||
                !symbol.Name.EndsWith(SymbolNames.ConfigureServicesMethodSuffix, StringComparison.OrdinalIgnoreCase))
            {
                return(false);
            }

            if (symbol.Parameters.Length != 1)
            {
                return(false);
            }

            return(SymbolEqualityComparer.Default.Equals(symbol.Parameters[0].Type, symbols.IServiceCollection));
        }
예제 #6
0
        public static List <IMethodSymbol> FindConfigureMethods(StartupSymbols symbols, IAssemblySymbol assembly)
        {
            var visitor = new ConfigureMethodVisitor(symbols);

            visitor.Visit(assembly);
            return(visitor._methods);
        }
예제 #7
0
        // Based on StartupLoader. The philosophy is that we want to do analysis only on things
        // that would be recognized as a Configure method to avoid false positives.
        //
        // The Configure method follows the naming pattern `Configure{Environment?}` (ignoring case).
        // The Configure method must be public.
        // The Configure method can be instance or static.
        // The Configure method *can* have other parameters besides IApplicationBuilder.
        //
        // The Configure method does not actually need to accept IApplicationBuilder
        // but we exclude that case because a Configure method that doesn't accept an
        // IApplicationBuilder can't do anything interesting to analysis.
        public static bool IsConfigure(StartupSymbols symbols, IMethodSymbol symbol)
        {
            if (symbol == null)
            {
                throw new ArgumentNullException(nameof(symbol));
            }

            if (symbol.DeclaredAccessibility != Accessibility.Public)
            {
                return(false);
            }

            if (symbol.Name == null ||
                !symbol.Name.StartsWith(SymbolNames.ConfigureMethodPrefix, StringComparison.OrdinalIgnoreCase))
            {
                return(false);
            }

            // IApplicationBuilder can appear in any parameter, but must appear.
            for (var i = 0; i < symbol.Parameters.Length; i++)
            {
                if (SymbolEqualityComparer.Default.Equals(symbol.Parameters[i].Type, symbols.IApplicationBuilder))
                {
                    return(true);
                }
            }

            return(false);
        }
예제 #8
0
        public async Task DetectFeaturesAsync_FindsNoFeatures()
        {
            // Arrange
            var source      = @"
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"");
            });
        }
    }
}";
            var compilation = TestCompilation.Create(source);
            var symbols     = new StartupSymbols(compilation);

            var type = (INamedTypeSymbol)compilation.GetSymbolsWithName("StartupWithNoFeatures").Single();

            Assert.True(StartupFacts.IsStartupClass(symbols, type));

            // Act
            var features = await CompilationFeatureDetector.DetectFeaturesAsync(compilation);

            // Assert
            Assert.Empty(features);
        }
예제 #9
0
        private void OnCompilationStart(CompilationStartAnalysisContext context)
        {
            var symbols = new StartupSymbols(context.Compilation);

            // Don't run analyzer if ASP.NET Core types cannot be found
            if (!symbols.HasRequiredSymbols)
            {
                return;
            }

            context.RegisterSymbolStartAction(context =>
            {
                var type = (INamedTypeSymbol)context.Symbol;
                if (!StartupFacts.IsStartupClass(symbols, type))
                {
                    // Not a startup class, nothing to do.
                    return;
                }

                // This analyzer fans out a bunch of jobs. The context will capture the results of doing analysis
                // on the startup code, so that other analyzers that run later can examine them.
                var builder = new StartupAnalysisBuilder(this, symbols);

                var services   = new ServicesAnalyzer(builder);
                var options    = new OptionsAnalyzer(builder);
                var middleware = new MiddlewareAnalyzer(builder);

                context.RegisterOperationBlockStartAction(context =>
                {
                    if (context.OwningSymbol.Kind != SymbolKind.Method)
                    {
                        return;
                    }

                    var method = (IMethodSymbol)context.OwningSymbol;
                    if (StartupFacts.IsConfigureServices(symbols, method))
                    {
                        OnConfigureServicesMethodFound(method);

                        services.AnalyzeConfigureServices(context);
                        options.AnalyzeConfigureServices(context);
                    }

                    if (StartupFacts.IsConfigure(symbols, method))
                    {
                        OnConfigureMethodFound(method);

                        middleware.AnalyzeConfigureMethod(context);
                    }
                });

                // Run after analyses have had a chance to finish to add diagnostics.
                context.RegisterSymbolEndAction(context =>
                {
                    var analysis = builder.Build();
                    new UseMvcAnalyzer(analysis).AnalyzeSymbol(context);
                    new BuildServiceProviderValidator(analysis).AnalyzeSymbol(context);
                });
            }, SymbolKind.NamedType);
        }
예제 #10
0
        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);
        }
예제 #11
0
        public static bool IsConfigure(StartupSymbols symbols, IMethodSymbol symbol)
        {
            if (symbol == null)
            {
                throw new ArgumentNullException(nameof(symbol));
            }

            if (symbol.DeclaredAccessibility != Accessibility.Public)
            {
                return(false);
            }

            if (symbol.Name == null || !symbol.Name.StartsWith("Configure", StringComparison.Ordinal))
            {
                return(false);
            }

            // IApplicationBuilder can appear in any parameter
            for (var i = 0; i < symbol.Parameters.Length; i++)
            {
                if (symbol.Parameters[i].Type == symbols.IApplicationBuilder)
                {
                    return(true);
                }
            }

            return(false);
        }
예제 #12
0
        public StartupAnalyzerContext(StartupAnalzyer analyzer, StartupSymbols startupSymbols)
        {
            _analyzer      = analyzer;
            StartupSymbols = startupSymbols;

            _analysesByType = new Dictionary <INamedTypeSymbol, List <object> >();
            _lock           = new object();
        }
예제 #13
0
        public StartupAnalysisBuilder(StartupAnalyzer analyzer, StartupSymbols startupSymbols)
        {
            _analyzer      = analyzer;
            StartupSymbols = startupSymbols;

#pragma warning disable RS1024 // Compare symbols correctly
            _analysesByType = new Dictionary <INamedTypeSymbol, List <object> >(SymbolEqualityComparer.Default);
#pragma warning restore RS1024 // Compare symbols correctly
            _lock = new object();
        }
예제 #14
0
        public void IsStartupClass_RejectsNotStartupClass(string source)
        {
            // Arrange
            var compilation = TestCompilation.Create(TestSources[source]);
            var symbols     = new StartupSymbols(compilation);

            var type = (INamedTypeSymbol)compilation.GetSymbolsWithName(source).Single();

            // Act
            var result = StartupFacts.IsStartupClass(symbols, type);

            // Assert
            Assert.False(result);
        }
예제 #15
0
        public async Task IsStartupClass_RejectsNotStartupClass(string source)
        {
            // Arrange
            var compilation = await CreateCompilationAsync(source);

            var symbols = new StartupSymbols(compilation);

            var type = (INamedTypeSymbol)compilation.GetSymbolsWithName(source).Single();

            // Act
            var result = StartupFacts.IsStartupClass(symbols, type);

            // Assert
            Assert.False(result);
        }
예제 #16
0
        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));
        }
예제 #17
0
        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);
        }
예제 #18
0
        public void IsConfigure_RejectsNonConfigureMethod(string source, string methodName)
        {
            // Arrange
            var compilation = TestCompilation.Create(TestSources[source]);
            var symbols     = new StartupSymbols(compilation);

            var type    = (INamedTypeSymbol)compilation.GetSymbolsWithName(source).Single();
            var methods = type.GetMembers(methodName).Cast <IMethodSymbol>();

            foreach (var method in methods)
            {
                // Act
                var result = StartupFacts.IsConfigure(symbols, method);

                // Assert
                Assert.False(result);
            }
        }
        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());
        }
예제 #20
0
        public async Task IsConfigure_FindsConfigureMethod(string source, string methodName)
        {
            // Arrange
            var compilation = await CreateCompilationAsync(source);

            var symbols = new StartupSymbols(compilation);

            var type    = (INamedTypeSymbol)compilation.GetSymbolsWithName(source).Single();
            var methods = type.GetMembers(methodName).Cast <IMethodSymbol>();

            foreach (var method in methods)
            {
                // Act
                var result = StartupFacts.IsConfigure(symbols, method);

                // Assert
                Assert.True(result);
            }
        }
예제 #21
0
        public async Task DetectFeatureAsync_StartupWithMapHub_FindsSignalR()
        {
            var source      = @"
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
    {
    }
}
";
            var compilation = TestCompilation.Create(source);
            var symbols     = new StartupSymbols(compilation);

            var type = (INamedTypeSymbol)compilation.GetSymbolsWithName("StartupWithMapHub").Single();

            Assert.True(StartupFacts.IsStartupClass(symbols, type));

            // Act
            var features = await CompilationFeatureDetector.DetectFeaturesAsync(compilation);

            // Assert
            Assert.Collection(features, f => Assert.Equal(WellKnownFeatures.SignalR, f));
        }
 private ConfigureMethodVisitor(StartupSymbols symbols)
 {
     _symbols = symbols;
     _methods = new List<IMethodSymbol>();
 }
        public void FindConfigureMethods_AtDifferentScopes()
        {
            // Arrange
            var source = @"
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)
            {
            }
        }
    }
}";

            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 = TestCompilation.Create(source);
            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);
        }
#pragma warning disable RS1012 // Start action has no registered actions.
        public StartupAnalysisContext(OperationBlockStartAnalysisContext operationBlockStartAnalysisContext, StartupSymbols startupSymbols)
        {
            OperationBlockStartAnalysisContext = operationBlockStartAnalysisContext;
            StartupSymbols = startupSymbols;
        }
예제 #25
0
        private void AnalyzeStartupMethods(OperationBlockStartAnalysisContext context, StartupSymbols symbols, ConcurrentBag <StartupComputedAnalysis> analyses)
        {
            if (!IsStartupFile(context))
            {
                return;
            }

            if (context.OwningSymbol.Kind != SymbolKind.Method)
            {
                return;
            }

            var startupAnalysisContext = new StartupAnalysisContext(context, symbols);

            var method = (IMethodSymbol)context.OwningSymbol;

            if (StartupFacts.IsConfigureServices(symbols, method))
            {
                for (var i = 0; i < ConfigureServicesMethodAnalysisFactories.Length; i++)
                {
                    var analysis = ConfigureServicesMethodAnalysisFactories[i].Invoke(startupAnalysisContext);
                    analyses.Add(analysis);

                    OnAnalysisStarted(analysis);
                }

                OnConfigureServicesMethodFound(method);
            }

            if (StartupFacts.IsConfigure(symbols, method))
            {
                for (var i = 0; i < ConfigureMethodAnalysisFactories.Length; i++)
                {
                    var analysis = ConfigureMethodAnalysisFactories[i].Invoke(startupAnalysisContext);
                    analyses.Add(analysis);

                    OnAnalysisStarted(analysis);
                }

                OnConfigureMethodFound(method);
            }
        }
예제 #26
0
        private void OnCompilationStart(CompilationStartAnalysisContext context)
        {
            var symbols = new StartupSymbols(context.Compilation);

            // Don't run analyzer if ASP.NET Core types cannot be found
            if (!symbols.HasRequiredSymbols)
            {
                return;
            }

            var entryPoint = context.Compilation.GetEntryPoint(context.CancellationToken);

            context.RegisterSymbolStartAction(context =>
            {
                var type = (INamedTypeSymbol)context.Symbol;
                if (!StartupFacts.IsStartupClass(symbols, type) && !SymbolEqualityComparer.Default.Equals(entryPoint?.ContainingType, type))
                {
                    // Not a startup class, nothing to do.
                    return;
                }

                // This analyzer fans out a bunch of jobs. The context will capture the results of doing analysis
                // on the startup code, so that other analyzers that run later can examine them.
                var builder = new StartupAnalysisBuilder(this, symbols);

                var services   = new ServicesAnalyzer(builder);
                var options    = new OptionsAnalyzer(builder);
                var middleware = new MiddlewareAnalyzer(builder);

                context.RegisterOperationBlockStartAction(context =>
                {
                    if (context.OwningSymbol.Kind != SymbolKind.Method)
                    {
                        return;
                    }

                    var method = (IMethodSymbol)context.OwningSymbol;
                    var isConfigureServices = StartupFacts.IsConfigureServices(symbols, method);
                    if (isConfigureServices)
                    {
                        OnConfigureServicesMethodFound(method);
                    }

                    // In the future we can consider looking at more methods, but for now limit to Main, implicit Main, and Configure* methods
                    var isMain = SymbolEqualityComparer.Default.Equals(entryPoint, context.OwningSymbol);

                    if (isConfigureServices || isMain)
                    {
                        services.AnalyzeConfigureServices(context);
                        options.AnalyzeConfigureServices(context);
                    }

                    var isConfigure = StartupFacts.IsConfigure(symbols, method);
                    if (isConfigure)
                    {
                        OnConfigureMethodFound(method);
                    }
                    if (isConfigure || isMain)
                    {
                        middleware.AnalyzeConfigureMethod(context);
                    }
                });

                // Run after analyses have had a chance to finish to add diagnostics.
                context.RegisterSymbolEndAction(context =>
                {
                    var analysis = builder.Build();
                    new UseMvcAnalyzer(analysis).AnalyzeSymbol(context);
                    new BuildServiceProviderAnalyzer(analysis).AnalyzeSymbol(context);
                    new UseAuthorizationAnalyzer(analysis).AnalyzeSymbol(context);
                });
            }, SymbolKind.NamedType);
        }