Skip to content

Commit d283852

Browse files
committed
Add analyzer to install OpenAPI package from extension methods
1 parent 49b72df commit d283852

File tree

6 files changed

+177
-2
lines changed

6 files changed

+177
-2
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Collections.Immutable;
6+
using System.Composition;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeFixes;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
12+
using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.AddPackage;
13+
14+
namespace Microsoft.AspNetCore.Analyzers.Dependencies;
15+
16+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddPackageFixer)), Shared]
17+
public sealed class AddPackageFixer : CodeFixProvider
18+
{
19+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
20+
{
21+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
22+
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
23+
if (root == null)
24+
{
25+
return;
26+
}
27+
28+
if (semanticModel == null)
29+
{
30+
return;
31+
}
32+
33+
var wellKnownTypes = WellKnownTypes.GetOrCreate(semanticModel.Compilation);
34+
35+
Dictionary<ThisAndExtensionMethod, PackageSourceAndNamespace> _wellKnownExtensionMethodCache = new()
36+
{
37+
{
38+
new(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_IServiceCollection), "AddOpenApi"),
39+
new("Microsoft.AspNetCore.OpenApi", "Microsoft.Extensions.DependencyInjection")
40+
},
41+
{
42+
new(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Routing_IEndpointRouteBuilder), "MapOpenApi"),
43+
new("Microsoft.AspNetCore.OpenApi", "Microsoft.AspNetCore.Builder")
44+
}
45+
};
46+
47+
foreach (var diagnostic in context.Diagnostics)
48+
{
49+
var location = diagnostic.Location.SourceSpan;
50+
var node = root.FindNode(location);
51+
if (node == null)
52+
{
53+
return;
54+
}
55+
var methodName = node is IdentifierNameSyntax identifier ? identifier.Identifier.Text : null;
56+
if (methodName == null)
57+
{
58+
return;
59+
}
60+
var symbol = semanticModel.GetSymbolInfo(((MemberAccessExpressionSyntax)node.Parent!).Expression).Symbol;
61+
var symbolType = symbol switch
62+
{
63+
IMethodSymbol methodSymbol => methodSymbol.ReturnType,
64+
IPropertySymbol propertySymbol => propertySymbol.Type,
65+
_ => null
66+
};
67+
68+
if (symbolType == null)
69+
{
70+
return;
71+
}
72+
73+
var targetThisAndExtensionMethod = new ThisAndExtensionMethod(symbolType, methodName);
74+
if (_wellKnownExtensionMethodCache.TryGetValue(targetThisAndExtensionMethod, out var packageSourceAndNamespace))
75+
{
76+
var position = diagnostic.Location.SourceSpan.Start;
77+
var packageInstallData = new AspNetCoreInstallPackageData(
78+
packageSource: null,
79+
packageName: packageSourceAndNamespace.packageName,
80+
packageVersionOpt: null,
81+
packageNamespaceName: packageSourceAndNamespace.namespaceName);
82+
var codeAction = await AspNetCoreAddPackageCodeAction.TryCreateCodeActionAsync(
83+
context.Document,
84+
position,
85+
packageInstallData,
86+
context.CancellationToken);
87+
88+
if (codeAction != null)
89+
{
90+
context.RegisterCodeFix(codeAction, diagnostic);
91+
}
92+
}
93+
94+
}
95+
}
96+
97+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
98+
99+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ["CS1061"];
100+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Analyzers;
5+
6+
internal record struct PackageSourceAndNamespace(string packageName, string namespaceName);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace Microsoft.AspNetCore.Analyzers;
8+
9+
internal readonly struct ThisAndExtensionMethod(ITypeSymbol thisType, string extensionMethod)
10+
{
11+
public ITypeSymbol ThisType { get; } = thisType;
12+
public string ExtensionMethod { get; } = extensionMethod;
13+
14+
public override bool Equals(object? obj)
15+
{
16+
if (obj is ThisAndExtensionMethod other)
17+
{
18+
return SymbolEqualityComparer.Default.Equals(ThisType, other.ThisType) &&
19+
ExtensionMethod.Equals(other.ExtensionMethod, StringComparison.OrdinalIgnoreCase);
20+
}
21+
return false;
22+
}
23+
24+
public override int GetHashCode() => HashCode.Combine(ThisType, ExtensionMethod);
25+
}

src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Microsoft.AspNetCore.App.CodeFixes.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@
2222
<ProjectReference Include="..\Analyzers\Microsoft.AspNetCore.App.Analyzers.csproj" />
2323
</ItemGroup>
2424

25+
<ItemGroup>
26+
<Compile Include="$(RepoRoot)\src\Shared\HashCode.cs" />
27+
</ItemGroup>
28+
2529
</Project>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpCodeFixVerifier<
5+
Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.WebApplicationBuilderAnalyzer,
6+
Microsoft.AspNetCore.Analyzers.Dependencies.AddPackageFixer>;
7+
8+
namespace Microsoft.AspNetCore.Analyzers.Dependencies;
9+
public class AddPackagesTest
10+
{
11+
[Fact]
12+
public async Task CanFixMissingExtensionMethod()
13+
{
14+
// Arrange
15+
var source = @"
16+
using Microsoft.AspNetCore.Builder;
17+
using Microsoft.Extensions.Hosting;
18+
using Microsoft.Extensions.Logging;
19+
20+
var builder = WebApplication.CreateBuilder(args);
21+
22+
builder.Services.{|CS1061:AddOpenApi|}();
23+
";
24+
var fixedSource = @"
25+
using Microsoft.AspNetCore.Builder;
26+
using Microsoft.Extensions.Hosting;
27+
using Microsoft.Extensions.DependencyInjection;
28+
using Microsoft.Extensions.Logging;
29+
30+
var builder = WebApplication.CreateBuilder(args);
31+
32+
builder.Services.AddOpenApi();
33+
";
34+
35+
// Assert
36+
await VerifyCS.VerifyCodeFixAsync(source, fixedSource);
37+
}
38+
}

src/Shared/RoslynUtils/WellKnownTypeData.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ public enum WellKnownType
112112
Microsoft_AspNetCore_Authorization_AuthorizeAttribute,
113113
Microsoft_Extensions_DependencyInjection_PolicyServiceCollectionExtensions,
114114
Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute,
115-
Microsoft_AspNetCore_Authorization_AuthorizationOptions
115+
Microsoft_AspNetCore_Authorization_AuthorizationOptions,
116+
Microsoft_Extensions_DependencyInjection_IServiceCollection
116117
}
117118

118119
public static string[] WellKnownTypeNames = new[]
@@ -222,6 +223,7 @@ public enum WellKnownType
222223
"Microsoft.AspNetCore.Authorization.AuthorizeAttribute",
223224
"Microsoft.Extensions.DependencyInjection.PolicyServiceCollectionExtensions",
224225
"Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute",
225-
"Microsoft.AspNetCore.Authorization.AuthorizationOptions"
226+
"Microsoft.AspNetCore.Authorization.AuthorizationOptions",
227+
"Microsoft.Extensions.DependencyInjection.IServiceCollection",
226228
};
227229
}

0 commit comments

Comments
 (0)