Skip to content

Add analyzer for custom IProblemDetailsWriter registration #49286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
237a63e
Add diagnostic
david-acker Jul 9, 2023
1839ce7
Include AddMvc in diagnostic message
david-acker Jul 9, 2023
1beee75
Add symbol names for MvcServiceCollectionExtensions
david-acker Jul 9, 2023
9850331
Add IProblemDetailsWriterAnalyzer
david-acker Jul 10, 2023
d3adbb8
Merge branch 'main' into custom-IProblemDetailsWriter-analyzer
david-acker Oct 13, 2023
0554712
Merge branch 'main' into custom-IProblemDetailsWriter-analyzer
david-acker Feb 5, 2024
5872af3
Merge branch 'main' into custom-IProblemDetailsWriter-analyzer
david-acker Feb 10, 2024
a26f92d
Merge branch 'main' into custom-IProblemDetailsWriter-analyzer
david-acker Feb 20, 2024
0c8b19a
Merge branch 'main' into custom-IProblemDetailsWriter-analyzer
david-acker Mar 12, 2024
de4941d
Merge branch 'main' into custom-IProblemDetailsWriter-analyzer
david-acker May 5, 2024
6f4a4b9
Move ProblemDetailsWriterAnalyzer, StartupAnalyzer infrastructure to …
david-acker May 5, 2024
3243e6c
Revert changes to legacy Analyzers project
david-acker May 5, 2024
5919473
Formatting, clean up
david-acker May 5, 2024
9390405
Merge branch 'main' into custom-IProblemDetailsWriter-analyzer
david-acker Jun 8, 2024
e483d13
Add IncorrectlyConfiguredProblemDetailsWriterFixer
david-acker Jun 8, 2024
814364b
Implement code fix
david-acker Jun 9, 2024
2e1c3f4
Fix method names
david-acker Jun 9, 2024
68c6e92
Don't fix IProblemDetailsWriter registrations in chained invocations
david-acker Jun 9, 2024
69bc174
Exclude chained expressions from code fix
david-acker Aug 10, 2024
c6bcbdc
Assert FixAll behavior in multiple PDWs test
david-acker Aug 10, 2024
a9c7114
Get parent expression statement for chained invocations
david-acker Aug 10, 2024
4468aec
Merge main
david-acker Aug 10, 2024
00dd60d
Remove extra line, unused properties in test
david-acker Aug 23, 2024
d132e6f
Revert unintended changes to resource file
david-acker Aug 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
<!-- Source code settings -->
<PropertyGroup>
<SharedSourceRoot>$(MSBuildThisFileDirectory)src\Shared\</SharedSourceRoot>
<AnalyzerSharedSourceRoot>$(MSBuildThisFileDirectory)src\Analyzers\Analyzers\src\</AnalyzerSharedSourceRoot>
<GoogleTestSubmoduleRoot>$(RepoRoot)src\submodules\googletest\</GoogleTestSubmoduleRoot>

<!-- Embed source files that are not tracked by the source control manager in the PDB. -->
Expand Down
6 changes: 3 additions & 3 deletions src/Analyzers/Analyzers/src/StartupSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ internal sealed class StartupSymbols
{
public StartupSymbols(Compilation compilation)
{
IApplicationBuilder = compilation.GetTypeByMetadataName(SymbolNames.IApplicationBuilder.MetadataName);
IServiceCollection = compilation.GetTypeByMetadataName(SymbolNames.IServiceCollection.MetadataName);
MvcOptions = compilation.GetTypeByMetadataName(SymbolNames.MvcOptions.MetadataName);
IApplicationBuilder = compilation.GetTypeByMetadataName(SymbolNames.IApplicationBuilder.MetadataName)!;
IServiceCollection = compilation.GetTypeByMetadataName(SymbolNames.IServiceCollection.MetadataName)!;
MvcOptions = compilation.GetTypeByMetadataName(SymbolNames.MvcOptions.MetadataName)!;
}

public bool HasRequiredSymbols => IApplicationBuilder != null && IServiceCollection != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,13 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");

internal static readonly DiagnosticDescriptor IncorrectlyConfiguredProblemDetailsWriter = new(
"ASP0027",
new LocalizableResourceString(nameof(Resources.Analyzer_IncorrectlyConfiguredProblemDetailsWriter_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.Analyzer_IncorrectlyConfiguredProblemDetailsWriter_Message), Resources.ResourceManager, typeof(Resources)),
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,17 @@
<Compile Include="$(SharedSourceRoot)Diagnostics\AnalyzerDebug.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(AnalyzerSharedSourceRoot)StartupFacts.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)StartupAnalysis.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)ServicesAnalysis.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)ServicesItem.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)OptionsAnalysis.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)OptionsItem.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)MiddlewareAnalysis.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)MiddlewareItem.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)StartupSymbols.cs" LinkBase="Shared" />
<Compile Include="$(AnalyzerSharedSourceRoot)SymbolNames.cs" LinkBase="Shared" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,10 @@
<data name="Analyzer_OverriddenAuthorizeAttribute_Title" xml:space="preserve">
<value>[Authorize] overridden by [AllowAnonymous] from farther away</value>
</data>
<data name="Analyzer_IncorrectlyConfiguredProblemDetailsWriter_Message" xml:space="preserve">
<value>The custom IProblemDetailsWriter must be registered before calling AddControllers, AddControllersWithViews, AddMvc, or AddRazorPages</value>
</data>
<data name="Analyzer_IncorrectlyConfiguredProblemDetailsWriter_Title" xml:space="preserve">
<value>Custom IProblemDetailsWriter is incorrectly configured</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.AspNetCore.Analyzers.Startup;

internal sealed 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 &&
SymbolEqualityComparer.Default.Equals(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()));
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.AspNetCore.Analyzers.Startup;

internal sealed 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 &&
// For nullable types, it's possible for Value to be null when HasValue is true.
operation.Value.ConstantValue.Value != null &&
operation.Target is IPropertyReferenceOperation property &&
property.Property?.ContainingType?.Name != null &&
property.Property.ContainingType.Name.EndsWith("Options", StringComparison.Ordinal))
{
options.Add(new OptionsItem(property.Property, operation.Value.ConstantValue.Value));
}

}, OperationKind.SimpleAssignment);

context.RegisterOperationBlockEndAction(context =>
{
_context.ReportAnalysis(new OptionsAnalysis(configureServicesMethod, options.ToImmutable()));
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.AspNetCore.Analyzers.Startup;

internal sealed class ProblemDetailsWriterAnalyzer
{
private readonly StartupAnalysis _context;

public ProblemDetailsWriterAnalyzer(StartupAnalysis context)
{
_context = context;
}

public void AnalyzeSymbol(SymbolAnalysisContext context)
{
Debug.Assert(context.Symbol.Kind == SymbolKind.NamedType);

var type = (INamedTypeSymbol)context.Symbol;

var serviceAnalyses = _context.GetRelatedAnalyses<ServicesAnalysis>(type);
if (serviceAnalyses == null)
{
return;
}

foreach (var serviceAnalysis in serviceAnalyses)
{
var mvcServiceItems = serviceAnalysis.Services
.Where(IsMvcServiceCollectionExtension)
.ToArray();

if (mvcServiceItems.Length == 0)
{
continue;
}

var problemDetailsWriterServiceItems = serviceAnalysis.Services
.Where(IsProblemDetailsWriterRegistration)
.ToArray();

if (problemDetailsWriterServiceItems.Length == 0)
{
continue;
}

var mvcServiceTextSpans = mvcServiceItems.ToDictionary(x => x, x => x.Operation.Syntax.Span);

foreach (var problemDetailsWriterServiceItem in problemDetailsWriterServiceItems)
{
var problemDetailsWriterServiceTextSpan = problemDetailsWriterServiceItem.Operation.Syntax.Span;

foreach (var mvcServiceTextSpan in mvcServiceTextSpans)
{
var mvcService = mvcServiceTextSpan.Key;
var textSpan = mvcServiceTextSpan.Value;

// Check if the IProblemDetailsWriter registration is after the MVC registration in the source.
if (problemDetailsWriterServiceTextSpan.CompareTo(textSpan) > 0)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.IncorrectlyConfiguredProblemDetailsWriter,
problemDetailsWriterServiceItem.Operation.Syntax.GetLocation(),
additionalLocations: [mvcService.Operation.Syntax.GetLocation()]));

break;
}
}
}
}
}

private static bool IsMvcServiceCollectionExtension(ServicesItem middlewareItem)
{
var methodName = middlewareItem.UseMethod.Name;

if (string.Equals(methodName, SymbolNames.MvcServiceCollectionExtensions.AddControllersMethodName, StringComparison.Ordinal)
|| string.Equals(methodName, SymbolNames.MvcServiceCollectionExtensions.AddControllersWithViewsMethodName, StringComparison.Ordinal)
|| string.Equals(methodName, SymbolNames.MvcServiceCollectionExtensions.AddMvcMethodName, StringComparison.Ordinal)
|| string.Equals(methodName, SymbolNames.MvcServiceCollectionExtensions.AddRazorPagesMethodName, StringComparison.Ordinal))
{
return true;
}

return false;
}

private static bool IsProblemDetailsWriterRegistration(ServicesItem servicesItem)
{
var methodName = servicesItem.UseMethod.Name;

if (string.Equals(methodName, SymbolNames.ServiceCollectionServiceExtensions.AddTransientMethodName, StringComparison.Ordinal)
|| string.Equals(methodName, SymbolNames.ServiceCollectionServiceExtensions.AddScopedMethodName, StringComparison.Ordinal)
|| string.Equals(methodName, SymbolNames.ServiceCollectionServiceExtensions.AddSingletonMethodName, StringComparison.Ordinal))
{
var typeArguments = servicesItem.Operation.TargetMethod.TypeArguments;

if (typeArguments.Length == 2
&& string.Equals(typeArguments[0].Name, SymbolNames.IProblemDetailsWriter.Name, StringComparison.Ordinal))
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.AspNetCore.Analyzers.Startup;

internal sealed 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 &&
SymbolEqualityComparer.Default.Equals(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()));
});
}
}
Loading
Loading