Refactor of Startup analyzer code

This commit is contained in:
Ryan Nowak 2019-03-24 14:53:08 -07:00 committed by Ryan Nowak
parent d98bde4fc7
commit 753e98c96d
25 changed files with 571 additions and 420 deletions

View File

@ -1,41 +1,42 @@
// 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.Concurrent;
using System.Linq;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Analyzers
{
internal class BuildServiceProviderValidator : StartupDiagnosticValidator
internal class BuildServiceProviderValidator
{
public static BuildServiceProviderValidator CreateAndInitialize(CompilationAnalysisContext context, ConcurrentBag<StartupComputedAnalysis> analyses)
private readonly StartupAnalysis _context;
public BuildServiceProviderValidator(StartupAnalysis context)
{
if (analyses == null)
{
throw new ArgumentNullException(nameof(analyses));
}
_context = context;
}
var validator = new BuildServiceProviderValidator();
public void AnalyzeSymbol(SymbolAnalysisContext context)
{
Debug.Assert(context.Symbol.Kind == SymbolKind.NamedType);
Debug.Assert(StartupFacts.IsStartupClass(_context.StartupSymbols, (INamedTypeSymbol)context.Symbol));
foreach (var serviceAnalysis in analyses.OfType<ServicesAnalysis>())
var type = (INamedTypeSymbol)context.Symbol;
foreach (var serviceAnalysis in _context.GetRelatedAnalyses<ServicesAnalysis>(type))
{
foreach (var serviceItem in serviceAnalysis.Services)
{
if (serviceItem.UseMethod.Name == "BuildServiceProvider")
{
context.ReportDiagnostic(Diagnostic.Create(
StartupAnalzyer.BuildServiceProviderShouldNotCalledInConfigureServicesMethod,
StartupAnalzyer.Diagnostics.BuildServiceProviderShouldNotCalledInConfigureServicesMethod,
serviceItem.Operation.Syntax.GetLocation(),
serviceItem.UseMethod.Name,
serviceAnalysis.ConfigureServicesMethod.Name));
}
}
}
return validator;
}
}
}

View File

@ -1,18 +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.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers
{
internal abstract class ConfigureMethodAnalysis : StartupComputedAnalysis
{
protected ConfigureMethodAnalysis(IMethodSymbol configureMethod)
: base(configureMethod.ContainingType)
{
ConfigureMethod = configureMethod;
}
public IMethodSymbol ConfigureMethod { get; }
}
}

View File

@ -1,53 +1,23 @@
// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.AspNetCore.Analyzers
{
internal class MiddlewareAnalysis : ConfigureMethodAnalysis
internal class MiddlewareAnalysis
{
public static MiddlewareAnalysis CreateAndInitialize(StartupAnalysisContext context)
public MiddlewareAnalysis(IMethodSymbol configureMethod, ImmutableArray<MiddlewareItem> middleware)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var symbols = context.StartupSymbols;
var analysis = new MiddlewareAnalysis((IMethodSymbol)context.OperationBlockStartAnalysisContext.OwningSymbol);
var middleware = ImmutableArray.CreateBuilder<MiddlewareItem>();
context.OperationBlockStartAnalysisContext.RegisterOperationAction(context =>
{
// We're looking for usage of extension methods, so we need to look at the 'this' parameter
// rather than invocation.Instance.
if (context.Operation is IInvocationOperation invocation &&
invocation.Instance == null &&
invocation.Arguments.Length >= 1 &&
invocation.Arguments[0].Parameter?.Type == symbols.IApplicationBuilder)
{
middleware.Add(new MiddlewareItem(invocation));
}
}, OperationKind.Invocation);
context.OperationBlockStartAnalysisContext.RegisterOperationBlockEndAction(context =>
{
analysis.Middleware = middleware.ToImmutable();
});
return analysis;
ConfigureMethod = configureMethod;
Middleware = middleware;
}
public MiddlewareAnalysis(IMethodSymbol configureMethod)
: base(configureMethod)
{
}
public INamedTypeSymbol StartupType => ConfigureMethod.ContainingType;
public ImmutableArray<MiddlewareItem> Middleware { get; private set; } = ImmutableArray<MiddlewareItem>.Empty;
public IMethodSymbol ConfigureMethod { get; }
public ImmutableArray<MiddlewareItem> Middleware { get; }
}
}

View File

@ -0,0 +1,48 @@
// 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.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.AspNetCore.Analyzers
{
internal class MiddlewareAnalyzer
{
private readonly StartupAnalysisBuilder _context;
public MiddlewareAnalyzer(StartupAnalysisBuilder context)
{
_context = context;
}
public void AnalyzeConfigureMethod(OperationBlockStartAnalysisContext context)
{
var configureMethod = (IMethodSymbol)context.OwningSymbol;
var middleware = ImmutableArray.CreateBuilder<MiddlewareItem>();
// Note: this is a simple source-order implementation. We don't attempt perform data flow
// analysis in order to determine the actual order in which middleware are ordered.
//
// This can currently be confused by things like Map(...)
context.RegisterOperationAction(context =>
{
// We're looking for usage of extension methods, so we need to look at the 'this' parameter
// rather than invocation.Instance.
if (context.Operation is IInvocationOperation invocation &&
invocation.Instance == null &&
invocation.Arguments.Length >= 1 &&
invocation.Arguments[0].Parameter?.Type == _context.StartupSymbols.IApplicationBuilder)
{
middleware.Add(new MiddlewareItem(invocation));
}
}, OperationKind.Invocation);
context.RegisterOperationBlockEndAction(context =>
{
_context.ReportAnalysis(new MiddlewareAnalysis(configureMethod, middleware.ToImmutable()));
});
}
}
}

View File

@ -1,45 +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 System;
using Microsoft.AspNetCore.Mvc.Analyzers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.AspNetCore.Analyzers
{
internal class MvcOptionsAnalysis : ConfigureServicesMethodAnalysis
{
public static MvcOptionsAnalysis CreateAndInitialize(StartupAnalysisContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var analysis = new MvcOptionsAnalysis((IMethodSymbol)context.OperationBlockStartAnalysisContext.OwningSymbol);
context.OperationBlockStartAnalysisContext.RegisterOperationAction(context =>
{
if (context.Operation is ISimpleAssignmentOperation operation &&
operation.Value.ConstantValue.HasValue &&
operation.Target is IPropertyReferenceOperation property &&
property.Member?.Name == SymbolNames.EnableEndpointRoutingProperty)
{
analysis.EndpointRoutingEnabled = operation.Value.ConstantValue.Value as bool?;
}
}, OperationKind.SimpleAssignment);
return analysis;
}
public MvcOptionsAnalysis(IMethodSymbol configureServicesMethod)
: base(configureServicesMethod)
{
}
public bool? EndpointRoutingEnabled { get; private set; }
}
}

View File

@ -1,18 +1,23 @@
// 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.Immutable;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers
{
internal abstract class ConfigureServicesMethodAnalysis : StartupComputedAnalysis
internal class OptionsAnalysis
{
protected ConfigureServicesMethodAnalysis(IMethodSymbol configureServicesMethod)
: base(configureServicesMethod.ContainingType)
public OptionsAnalysis(IMethodSymbol configureServicesMethod, ImmutableArray<OptionsItem> options)
{
ConfigureServicesMethod = configureServicesMethod;
Options = options;
}
public INamedTypeSymbol StartupType => ConfigureServicesMethod.ContainingType;
public IMethodSymbol ConfigureServicesMethod { get; }
public ImmutableArray<OptionsItem> Options { get; }
}
}

View File

@ -0,0 +1,43 @@
// 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.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.AspNetCore.Analyzers
{
internal class OptionsAnalyzer
{
private readonly StartupAnalysisBuilder _context;
public OptionsAnalyzer(StartupAnalysisBuilder context)
{
_context = context;
}
public void AnalyzeConfigureServices(OperationBlockStartAnalysisContext context)
{
var configureServicesMethod = (IMethodSymbol)context.OwningSymbol;
var options = ImmutableArray.CreateBuilder<OptionsItem>();
context.RegisterOperationAction(context =>
{
if (context.Operation is ISimpleAssignmentOperation operation &&
operation.Value.ConstantValue.HasValue &&
operation.Target is IPropertyReferenceOperation property &&
property.Property?.ContainingType?.Name != null &&
property.Property.ContainingType.Name.EndsWith("Options"))
{
options.Add(new OptionsItem(property.Property, operation.Value.ConstantValue.Value));
}
}, OperationKind.SimpleAssignment);
context.RegisterOperationBlockEndAction(context =>
{
_context.ReportAnalysis(new OptionsAnalysis(configureServicesMethod, options.ToImmutable()));
});
}
}
}

View File

@ -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 System;
using Microsoft.AspNetCore.Mvc.Analyzers;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers
{
internal static class OptionsFacts
{
public static bool IsEndpointRoutingExplicitlyDisabled(OptionsAnalysis analysis)
{
for (var i = 0; i < analysis.Options.Length; i++)
{
var item = analysis.Options[i];
if (string.Equals(item.OptionsType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), SymbolNames.MvcOptions) &&
string.Equals(item.Property.Name, SymbolNames.EnableEndpointRoutingProperty, StringComparison.Ordinal))
{
return item.ConstantValue as bool? == false;
}
}
return false;
}
}
}

View File

@ -0,0 +1,22 @@
// 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 class OptionsItem
{
public OptionsItem(IPropertySymbol property, object constantValue)
{
Property = property;
ConstantValue = constantValue;
}
public INamedTypeSymbol OptionsType => Property.ContainingType;
public IPropertySymbol Property { get; }
public object ConstantValue { get; }
}
}

View File

@ -1,52 +1,23 @@
// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.AspNetCore.Analyzers
{
internal class ServicesAnalysis : ConfigureServicesMethodAnalysis
internal class ServicesAnalysis
{
public static ServicesAnalysis CreateAndInitialize(StartupAnalysisContext context)
public ServicesAnalysis(IMethodSymbol configureServicesMethod, ImmutableArray<ServicesItem> services)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var symbols = context.StartupSymbols;
var analysis = new ServicesAnalysis((IMethodSymbol)context.OperationBlockStartAnalysisContext.OwningSymbol);
var services = ImmutableArray.CreateBuilder<ServicesItem>();
context.OperationBlockStartAnalysisContext.RegisterOperationAction(context =>
{
// We're looking for usage of extension methods, so we need to look at the 'this' parameter
// rather than invocation.Instance.
if (context.Operation is IInvocationOperation invocation &&
invocation.Instance == null &&
invocation.Arguments.Length >= 1 &&
invocation.Arguments[0].Parameter?.Type == symbols.IServiceCollection)
{
services.Add(new ServicesItem(invocation));
}
}, OperationKind.Invocation);
context.OperationBlockStartAnalysisContext.RegisterOperationBlockEndAction(context =>
{
analysis.Services = services.ToImmutable();
});
return analysis;
ConfigureServicesMethod = configureServicesMethod;
Services = services;
}
public ServicesAnalysis(IMethodSymbol configureServicesMethod)
: base(configureServicesMethod)
{
}
public INamedTypeSymbol StartupType => ConfigureServicesMethod.ContainingType;
public ImmutableArray<ServicesItem> Services { get; private set; } = ImmutableArray<ServicesItem>.Empty;
public IMethodSymbol ConfigureServicesMethod { get; }
public ImmutableArray<ServicesItem> Services { get; }
}
}

View File

@ -0,0 +1,43 @@
// 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.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.AspNetCore.Analyzers
{
internal class ServicesAnalyzer
{
private readonly StartupAnalysisBuilder _context;
public ServicesAnalyzer(StartupAnalysisBuilder context)
{
_context = context;
}
public void AnalyzeConfigureServices(OperationBlockStartAnalysisContext context)
{
var configureServicesMethod = (IMethodSymbol)context.OwningSymbol;
var services = ImmutableArray.CreateBuilder<ServicesItem>();
context.RegisterOperationAction(context =>
{
// We're looking for usage of extension methods, so we need to look at the 'this' parameter
// rather than invocation.Instance.
if (context.Operation is IInvocationOperation invocation &&
invocation.Instance == null &&
invocation.Arguments.Length >= 1 &&
invocation.Arguments[0].Parameter?.Type == _context.StartupSymbols.IServiceCollection)
{
services.Add(new ServicesItem(invocation));
}
}, OperationKind.Invocation);
context.RegisterOperationBlockEndAction(context =>
{
_context.ReportAnalysis(new ServicesAnalysis(configureServicesMethod, services.ToImmutable()));
});
}
}
}

View File

@ -0,0 +1,56 @@
// 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.Immutable;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers
{
internal class StartupAnalysis
{
private ImmutableDictionary<INamedTypeSymbol, ImmutableArray<object>> _analysesByType;
public StartupAnalysis(
StartupSymbols startupSymbols,
ImmutableDictionary<INamedTypeSymbol, ImmutableArray<object>> analysesByType)
{
StartupSymbols = startupSymbols;
_analysesByType = analysesByType;
}
public StartupSymbols StartupSymbols { get; }
public T? GetRelatedSingletonAnalysis<T>(INamedTypeSymbol type) where T : class
{
if (_analysesByType.TryGetValue(type, out var list))
{
for (var i = 0; i < list.Length; i++)
{
if (list[i] is T item)
{
return item;
}
}
}
return null;
}
public ImmutableArray<T> GetRelatedAnalyses<T>(INamedTypeSymbol type) where T : class
{
var items = ImmutableArray.CreateBuilder<T>();
if (_analysesByType.TryGetValue(type, out var list))
{
for (var i = 0; i < list.Length; i++)
{
if (list[i] is T item)
{
items.Add(item);
}
}
}
return items.ToImmutable();
}
}
}

View File

@ -0,0 +1,71 @@
// 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 Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers
{
internal class StartupAnalysisBuilder
{
private readonly Dictionary<INamedTypeSymbol, List<object>> _analysesByType;
private readonly StartupAnalzyer _analyzer;
private readonly object _lock;
public StartupAnalysisBuilder(StartupAnalzyer analyzer, StartupSymbols startupSymbols)
{
_analyzer = analyzer;
StartupSymbols = startupSymbols;
_analysesByType = new Dictionary<INamedTypeSymbol, List<object>>();
_lock = new object();
}
public StartupSymbols StartupSymbols { get; }
public StartupAnalysis Build()
{
lock (_lock)
{
return new StartupAnalysis(
StartupSymbols,
_analysesByType.ToImmutableDictionary(
k => k.Key,
v => v.Value.ToImmutableArray()));
}
}
public void ReportAnalysis(ServicesAnalysis analysis)
{
ReportAnalysisCore(analysis.StartupType, analysis);
_analyzer.OnServicesAnalysisCompleted(analysis);
}
public void ReportAnalysis(OptionsAnalysis analysis)
{
ReportAnalysisCore(analysis.StartupType, analysis);
_analyzer.OnOptionsAnalysisCompleted(analysis);
}
public void ReportAnalysis(MiddlewareAnalysis analysis)
{
ReportAnalysisCore(analysis.StartupType, analysis);
_analyzer.OnMiddlewareAnalysisCompleted(analysis);
}
private void ReportAnalysisCore(INamedTypeSymbol type, object analysis)
{
lock (_lock)
{
if (!_analysesByType.TryGetValue(type, out var list))
{
list = new List<object>();
_analysesByType.Add(type, list);
}
list.Add(analysis);
}
}
}
}

View File

@ -1,22 +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.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Analyzers
{
internal class StartupAnalysisContext
{
#pragma warning disable RS1012 // Start action has no registered actions.
public StartupAnalysisContext(OperationBlockStartAnalysisContext operationBlockStartAnalysisContext, StartupSymbols startupSymbols)
{
OperationBlockStartAnalysisContext = operationBlockStartAnalysisContext;
StartupSymbols = startupSymbols;
}
public OperationBlockStartAnalysisContext OperationBlockStartAnalysisContext { get; }
public StartupSymbols StartupSymbols { get; }
#pragma warning restore RS1012 // Start action has no registered actions.
}
}

View File

@ -1,6 +1,7 @@
// 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.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
@ -8,23 +9,39 @@ namespace Microsoft.AspNetCore.Analyzers
{
public partial class StartupAnalzyer : DiagnosticAnalyzer
{
internal readonly static DiagnosticDescriptor UnsupportedUseMvcWithEndpointRouting = new DiagnosticDescriptor(
"MVC1005",
"Cannot use UseMvc with Endpoint Routing.",
"Using '{0}' to configure MVC is not supported while using Endpoint Routing. To continue using '{0}', please set 'MvcOptions.EnableEndpointRouting = false' inside '{1}'.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/YJggeFn");
internal static class Diagnostics
{
public static readonly ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics;
internal readonly static DiagnosticDescriptor BuildServiceProviderShouldNotCalledInConfigureServicesMethod = new DiagnosticDescriptor(
"MVC1007",
"Do not call 'IServiceCollection.BuildServiceProvider' in 'ConfigureServices'",
"Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'.",
"Design",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/AA5k895"
);
static Diagnostics()
{
SupportedDiagnostics = ImmutableArray.Create<DiagnosticDescriptor>(new[]
{
// ASP
BuildServiceProviderShouldNotCalledInConfigureServicesMethod,
// MVC
UnsupportedUseMvcWithEndpointRouting,
});
}
internal readonly static DiagnosticDescriptor BuildServiceProviderShouldNotCalledInConfigureServicesMethod = new DiagnosticDescriptor(
"ASP0000",
"Do not call 'IServiceCollection.BuildServiceProvider' in 'ConfigureServices'",
"Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'.",
"Design",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/AA5k895");
internal readonly static DiagnosticDescriptor UnsupportedUseMvcWithEndpointRouting = new DiagnosticDescriptor(
"MVC1005",
"Cannot use UseMvc with Endpoint Routing.",
"Using '{0}' to configure MVC is not supported while using Endpoint Routing. To continue using '{0}', please set 'MvcOptions.EnableEndpointRounting = false' inside '{1}'.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/YJggeFn");
}
}
}

View File

@ -7,27 +7,42 @@ using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Analyzers
{
// Events for testability. Allows us to unit test the data we gather from analysis.
public partial class StartupAnalzyer : DiagnosticAnalyzer
{
internal event EventHandler<StartupComputedAnalysis>? AnalysisStarted;
private void OnAnalysisStarted(StartupComputedAnalysis analysis)
{
AnalysisStarted?.Invoke(this, analysis);
}
internal event EventHandler<IMethodSymbol>? ConfigureServicesMethodFound;
private void OnConfigureServicesMethodFound(IMethodSymbol method)
internal void OnConfigureServicesMethodFound(IMethodSymbol method)
{
ConfigureServicesMethodFound?.Invoke(this, method);
}
internal event EventHandler<ServicesAnalysis>? ServicesAnalysisCompleted;
internal void OnServicesAnalysisCompleted(ServicesAnalysis analysis)
{
ServicesAnalysisCompleted?.Invoke(this, analysis);
}
internal event EventHandler<OptionsAnalysis>? OptionsAnalysisCompleted;
internal void OnOptionsAnalysisCompleted(OptionsAnalysis analysis)
{
OptionsAnalysisCompleted?.Invoke(this, analysis);
}
internal event EventHandler<IMethodSymbol>? ConfigureMethodFound;
private void OnConfigureMethodFound(IMethodSymbol method)
internal void OnConfigureMethodFound(IMethodSymbol method)
{
ConfigureMethodFound?.Invoke(this, method);
}
internal event EventHandler<MiddlewareAnalysis>? MiddlewareAnalysisCompleted;
internal void OnMiddlewareAnalysisCompleted(MiddlewareAnalysis analysis)
{
MiddlewareAnalysisCompleted?.Invoke(this, analysis);
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
@ -12,46 +11,16 @@ namespace Microsoft.AspNetCore.Analyzers
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public partial class StartupAnalzyer : DiagnosticAnalyzer
{
#pragma warning disable RS1008 // Avoid storing per-compilation data into the fields of a diagnostic analyzer.
private readonly static Func<StartupAnalysisContext, StartupComputedAnalysis>[] ConfigureServicesMethodAnalysisFactories = new Func<StartupAnalysisContext, StartupComputedAnalysis>[]
{
ServicesAnalysis.CreateAndInitialize,
MvcOptionsAnalysis.CreateAndInitialize,
};
private readonly static Func<StartupAnalysisContext, StartupComputedAnalysis>[] ConfigureMethodAnalysisFactories = new Func<StartupAnalysisContext, StartupComputedAnalysis>[]
{
MiddlewareAnalysis.CreateAndInitialize,
};
private readonly static Func<CompilationAnalysisContext, ConcurrentBag<StartupComputedAnalysis>, StartupDiagnosticValidator>[] DiagnosticValidatorFactories = new Func<CompilationAnalysisContext, ConcurrentBag<StartupComputedAnalysis>, StartupDiagnosticValidator>[]
{
UseMvcDiagnosticValidator.CreateAndInitialize,
BuildServiceProviderValidator.CreateAndInitialize
};
#pragma warning restore RS1008 // Avoid storing per-compilation data into the fields of a diagnostic analyzer.
public StartupAnalzyer()
{
SupportedDiagnostics = ImmutableArray.Create<DiagnosticDescriptor>(new[]
{
UnsupportedUseMvcWithEndpointRouting,
BuildServiceProviderShouldNotCalledInConfigureServicesMethod
});
// By default the analyzer will only run for files ending with Startup.cs
// Can be overriden for unit testing other file names
// Analzyer only runs for C# so limiting to *.cs file is fine
StartupFilePredicate = path => path.EndsWith("Startup.cs", StringComparison.OrdinalIgnoreCase);
}
internal Func<string, bool> StartupFilePredicate { get; set; }
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => Diagnostics.SupportedDiagnostics;
public override void Initialize(AnalysisContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(OnCompilationStart);
}
@ -61,91 +30,61 @@ namespace Microsoft.AspNetCore.Analyzers
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)
if (!symbols.HasRequiredSymbols)
{
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 =>
context.RegisterSymbolStartAction(context =>
{
AnalyzeStartupMethods(context, symbols, analyses);
});
// Run after analyses have had a chance to finish to add diagnostics.
context.RegisterCompilationEndAction(analysisContext =>
{
RunAnalysis(analysisContext, analyses);
});
}
private static void RunAnalysis(CompilationAnalysisContext analysisContext, ConcurrentBag<StartupComputedAnalysis> analyses)
{
for (var i = 0; i < DiagnosticValidatorFactories.Length; i++)
{
var validator = DiagnosticValidatorFactories[i].Invoke(analysisContext, analyses);
}
}
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 type = (INamedTypeSymbol)context.Symbol;
if (!StartupFacts.IsStartupClass(symbols, type))
{
var analysis = ConfigureServicesMethodAnalysisFactories[i].Invoke(startupAnalysisContext);
analyses.Add(analysis);
OnAnalysisStarted(analysis);
// Not a startup class, nothing to do.
return;
}
OnConfigureServicesMethodFound(method);
}
// 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);
if (StartupFacts.IsConfigure(symbols, method))
{
for (var i = 0; i < ConfigureMethodAnalysisFactories.Length; i++)
var services = new ServicesAnalyzer(builder);
var options = new OptionsAnalyzer(builder);
var middleware = new MiddlewareAnalyzer(builder);
context.RegisterOperationBlockStartAction(context =>
{
var analysis = ConfigureMethodAnalysisFactories[i].Invoke(startupAnalysisContext);
analyses.Add(analysis);
if (context.OwningSymbol.Kind != SymbolKind.Method)
{
return;
}
OnAnalysisStarted(analysis);
}
var method = (IMethodSymbol)context.OwningSymbol;
if (StartupFacts.IsConfigureServices(symbols, method))
{
OnConfigureServicesMethodFound(method);
OnConfigureMethodFound(method);
}
}
#pragma warning disable RS1012 // Start action has no registered actions.
private bool IsStartupFile(OperationBlockStartAnalysisContext context)
{
foreach (var location in context.OwningSymbol.Locations)
{
if (location.IsInSource && StartupFilePredicate(location.SourceTree.FilePath))
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 =>
{
return true;
}
}
var analysis = builder.Build();
new UseMvcAnalyzer(analysis).AnalyzeSymbol(context);
new BuildServiceProviderValidator(analysis).AnalyzeSymbol(context);
});
return false;
}, SymbolKind.NamedType);
}
#pragma warning restore RS1012 // Start action has no registered actions.
}
}

View File

@ -1,17 +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.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers
{
internal abstract class StartupComputedAnalysis
{
protected StartupComputedAnalysis(INamedTypeSymbol enclosingType)
{
EnclosingType = enclosingType;
}
public INamedTypeSymbol EnclosingType { get; }
}
}

View File

@ -1,9 +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.
namespace Microsoft.AspNetCore.Analyzers
{
internal abstract class StartupDiagnosticValidator
{
}
}

View File

@ -2,13 +2,41 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.Analyzers;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers
{
internal static class StartupFacts
{
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 ConfigureServices or Configure as a hueristic.
// ConfigureServices might not appear in trivial cases, and Configure might be named ConfigureDevelopment
// or something similar.
//
// Since we already are analyzing the symbol it should be cheap to do a quick 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;
}
public static bool IsConfigureServices(StartupSymbols symbols, IMethodSymbol symbol)
{
if (symbol == null)
@ -21,7 +49,7 @@ namespace Microsoft.AspNetCore.Analyzers
return false;
}
if (!string.Equals(symbol.Name, SymbolNames.ConfigureServicesMethod, StringComparison.Ordinal))
if (!string.Equals(symbol.Name, "ConfigureServices", StringComparison.Ordinal))
{
return false;
}
@ -51,7 +79,7 @@ namespace Microsoft.AspNetCore.Analyzers
return false;
}
if (symbol.Name == null || !symbol.Name.StartsWith(SymbolNames.ConfigureMethod, StringComparison.Ordinal))
if (symbol.Name == null || !symbol.Name.StartsWith("Configure", StringComparison.Ordinal))
{
return false;
}
@ -59,7 +87,7 @@ namespace Microsoft.AspNetCore.Analyzers
// IApplicationBuilder can appear in any parameter
for (var i = 0; i < symbol.Parameters.Length; i++)
{
if (symbol.Parameters[i].Type == symbols.IApplicationBuilder)
if (symbol.Parameters[i].Type == symbols.IApplicationBuilder)
{
return true;
}

View File

@ -1,7 +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.AspNetCore.Mvc.Analyzers;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers
@ -10,15 +9,15 @@ namespace Microsoft.AspNetCore.Analyzers
{
public StartupSymbols(Compilation compilation)
{
IApplicationBuilder = compilation.GetTypeByMetadataName(SymbolNames.IApplicationBuilder);
IServiceCollection = compilation.GetTypeByMetadataName(SymbolNames.IServiceCollection);
MvcOptions = compilation.GetTypeByMetadataName(SymbolNames.MvcOptions);
IApplicationBuilder = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IApplicationBuilder");
IServiceCollection = compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.IServiceCollection");
MvcOptions = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.MvcOptions");
}
public bool HasRequiredSymbols => IApplicationBuilder != null && IServiceCollection != null;
public INamedTypeSymbol IApplicationBuilder { get; }
public INamedTypeSymbol IServiceCollection { get; }
public INamedTypeSymbol MvcOptions { get; }
}
}

View File

@ -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 System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Analyzers
{
internal class UseMvcAnalyzer
{
private readonly StartupAnalysis _context;
public UseMvcAnalyzer(StartupAnalysis context)
{
_context = context;
}
public void AnalyzeSymbol(SymbolAnalysisContext context)
{
Debug.Assert(context.Symbol.Kind == SymbolKind.NamedType);
Debug.Assert(StartupFacts.IsStartupClass(_context.StartupSymbols, (INamedTypeSymbol)context.Symbol));
var type = (INamedTypeSymbol)context.Symbol;
var optionsAnalysis = _context.GetRelatedSingletonAnalysis<OptionsAnalysis>(type);
if (optionsAnalysis == null)
{
return;
}
// Find the middleware analysis foreach of the Configure methods defined by this class and validate.
//
// Note that this doesn't attempt to handle inheritance scenarios.
foreach (var middlewareAnalysis in _context.GetRelatedAnalyses<MiddlewareAnalysis>(type))
{
foreach (var middlewareItem in middlewareAnalysis.Middleware)
{
if (middlewareItem.UseMethod.Name == "UseMvc" || middlewareItem.UseMethod.Name == "UseMvcWithDefaultRoute")
{
// Report a diagnostic if it's unclear that the user turned off Endpoint Routing.
if (!OptionsFacts.IsEndpointRoutingExplicitlyDisabled(optionsAnalysis))
{
context.ReportDiagnostic(Diagnostic.Create(
StartupAnalzyer.Diagnostics.UnsupportedUseMvcWithEndpointRouting,
middlewareItem.Operation.Syntax.GetLocation(),
middlewareItem.UseMethod.Name,
optionsAnalysis.ConfigureServicesMethod.Name));
}
}
}
}
}
}
}

View File

@ -1,51 +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 System;
using System.Collections.Concurrent;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Analyzers
{
internal class UseMvcDiagnosticValidator : StartupDiagnosticValidator
{
public static UseMvcDiagnosticValidator CreateAndInitialize(CompilationAnalysisContext context, ConcurrentBag<StartupComputedAnalysis> analyses)
{
if (analyses == null)
{
throw new ArgumentNullException(nameof(analyses));
}
var validator = new UseMvcDiagnosticValidator();
foreach (var mvcOptionsAnalysis in analyses.OfType<MvcOptionsAnalysis>())
{
// Each analysis of the options is one-per-class (in user code). Find the middleware analysis foreach of the Configure methods
// defined by this class and validate.
//
// Note that this doesn't attempt to handle inheritance scenarios.
foreach (var middlewareAnalsysis in analyses.OfType<MiddlewareAnalysis>().Where(m => m.EnclosingType == mvcOptionsAnalysis.EnclosingType))
{
foreach (var middlewareItem in middlewareAnalsysis.Middleware)
{
if ((middlewareItem.UseMethod.Name == "UseMvc" || middlewareItem.UseMethod.Name == "UseMvcWithDefaultRoute") &&
// Report a diagnostic if it's unclear that the user turned off Endpoint Routing.
(mvcOptionsAnalysis.EndpointRoutingEnabled == true || mvcOptionsAnalysis.EndpointRoutingEnabled == null))
{
context.ReportDiagnostic(Diagnostic.Create(
StartupAnalzyer.UnsupportedUseMvcWithEndpointRouting,
middlewareItem.Operation.Syntax.GetLocation(),
middlewareItem.UseMethod.Name,
mvcOptionsAnalysis.ConfigureServicesMethod.Name));
}
}
}
}
return validator;
}
}
}

View File

@ -17,14 +17,15 @@ namespace Microsoft.AspNetCore.Analyzers
public StartupAnalyzerTest()
{
StartupAnalyzer = new StartupAnalzyer();
StartupAnalyzer.StartupFilePredicate = path => path.Equals("Test.cs", StringComparison.Ordinal);
Runner = new MvcDiagnosticAnalyzerRunner(StartupAnalyzer);
Analyses = new ConcurrentBag<StartupComputedAnalysis>();
Analyses = new ConcurrentBag<object>();
ConfigureServicesMethods = new ConcurrentBag<IMethodSymbol>();
ConfigureMethods = new ConcurrentBag<IMethodSymbol>();
StartupAnalyzer.AnalysisStarted += (sender, analysis) => Analyses.Add(analysis);
StartupAnalyzer.ServicesAnalysisCompleted += (sender, analysis) => Analyses.Add(analysis);
StartupAnalyzer.OptionsAnalysisCompleted += (sender, analysis) => Analyses.Add(analysis);
StartupAnalyzer.MiddlewareAnalysisCompleted += (sender, analysis) => Analyses.Add(analysis);
StartupAnalyzer.ConfigureServicesMethodFound += (sender, method) => ConfigureServicesMethods.Add(method);
StartupAnalyzer.ConfigureMethodFound += (sender, method) => ConfigureMethods.Add(method);
}
@ -33,7 +34,7 @@ namespace Microsoft.AspNetCore.Analyzers
private MvcDiagnosticAnalyzerRunner Runner { get; }
private ConcurrentBag<StartupComputedAnalysis> Analyses { get; }
private ConcurrentBag<object> Analyses { get; }
private ConcurrentBag<IMethodSymbol> ConfigureServicesMethods { get; }
@ -87,8 +88,8 @@ namespace Microsoft.AspNetCore.Analyzers
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var mvcOptionsAnalysis = Assert.Single(Analyses.OfType<MvcOptionsAnalysis>());
Assert.False(mvcOptionsAnalysis.EndpointRoutingEnabled);
var optionsAnalysis = Assert.Single(Analyses.OfType<OptionsAnalysis>());
Assert.True(OptionsFacts.IsEndpointRoutingExplicitlyDisabled(optionsAnalysis));
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
var middleware = Assert.Single(middlewareAnalysis.Middleware);
@ -107,8 +108,8 @@ namespace Microsoft.AspNetCore.Analyzers
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var mvcOptionsAnalysis = Assert.Single(Analyses.OfType<MvcOptionsAnalysis>());
Assert.False(mvcOptionsAnalysis.EndpointRoutingEnabled);
var optionsAnalysis = Assert.Single(Analyses.OfType<OptionsAnalysis>());
Assert.True(OptionsFacts.IsEndpointRoutingExplicitlyDisabled(optionsAnalysis));
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
var middleware = Assert.Single(middlewareAnalysis.Middleware);
@ -130,8 +131,8 @@ namespace Microsoft.AspNetCore.Analyzers
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var mvcOptionsAnalysis = Assert.Single(Analyses.OfType<MvcOptionsAnalysis>());
Assert.Null(mvcOptionsAnalysis.EndpointRoutingEnabled);
var optionsAnalysis = Assert.Single(Analyses.OfType<OptionsAnalysis>());
Assert.False(OptionsFacts.IsEndpointRoutingExplicitlyDisabled(optionsAnalysis));
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
var middleware = Assert.Single(middlewareAnalysis.Middleware);
@ -141,7 +142,7 @@ namespace Microsoft.AspNetCore.Analyzers
diagnostics,
diagnostic =>
{
Assert.Same(StartupAnalzyer.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
Assert.Same(StartupAnalzyer.Diagnostics.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
});
}
@ -156,8 +157,8 @@ namespace Microsoft.AspNetCore.Analyzers
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var mvcOptionsAnalysis = Assert.Single(Analyses.OfType<MvcOptionsAnalysis>());
Assert.Null(mvcOptionsAnalysis.EndpointRoutingEnabled);
var optionsAnalysis = Assert.Single(Analyses.OfType<OptionsAnalysis>());
Assert.False(OptionsFacts.IsEndpointRoutingExplicitlyDisabled(optionsAnalysis));
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
@ -173,7 +174,7 @@ namespace Microsoft.AspNetCore.Analyzers
diagnostics,
diagnostic =>
{
Assert.Same(StartupAnalzyer.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
Assert.Same(StartupAnalzyer.Diagnostics.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
});
}
@ -188,27 +189,28 @@ namespace Microsoft.AspNetCore.Analyzers
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var mvcOptionsAnalysis = Assert.Single(Analyses.OfType<MvcOptionsAnalysis>());
Assert.Null(mvcOptionsAnalysis.EndpointRoutingEnabled);
var optionsAnalysis = Assert.Single(Analyses.OfType<OptionsAnalysis>());
Assert.False(OptionsFacts.IsEndpointRoutingExplicitlyDisabled(optionsAnalysis));
Assert.Collection(
diagnostics,
diagnostic =>
{
Assert.Same(StartupAnalzyer.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
Assert.Same(StartupAnalzyer.Diagnostics.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic.Location);
},
diagnostic =>
{
Assert.Same(StartupAnalzyer.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
Assert.Same(StartupAnalzyer.Diagnostics.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM2"], diagnostic.Location);
},
diagnostic =>
{
Assert.Same(StartupAnalzyer.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
Assert.Same(StartupAnalzyer.Diagnostics.UnsupportedUseMvcWithEndpointRouting, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM3"], diagnostic.Location);
});
}
[Fact]
public async Task StartupAnalyzer_ServicesAnalysis_CallBuildServiceProvider()
{
@ -224,10 +226,11 @@ namespace Microsoft.AspNetCore.Analyzers
Assert.Collection(diagnostics,
diagnostic =>
{
Assert.Same(StartupAnalzyer.BuildServiceProviderShouldNotCalledInConfigureServicesMethod, diagnostic.Descriptor);
Assert.Same(StartupAnalzyer.Diagnostics.BuildServiceProviderShouldNotCalledInConfigureServicesMethod, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic.Location);
});
}
private TestSource ReadSource(string fileName)
{
return MvcTestSource.Read(nameof(StartupAnalyzerTest), fileName);

View File

@ -205,7 +205,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute)
.WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.AttributeArgumentList().AddArguments(
SyntaxFactory.AttributeArgument(statusCodeSyntax)));
}