Skip to content
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

Add codefixer and completion provider to install OpenAPI package from extension methods #55963

Merged
merged 10 commits into from
Jun 14, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.AddPackage;

namespace Microsoft.AspNetCore.Analyzers.Dependencies;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddPackageFixer)), Shared]
public class AddPackageFixer : CodeFixProvider
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
{
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
if (root == null)
{
return;
}

if (semanticModel == null)
{
return;
}

var wellKnownTypes = WellKnownTypes.GetOrCreate(semanticModel.Compilation);

Dictionary<ThisAndExtensionMethod, PackageSourceAndNamespace> _wellKnownExtensionMethodCache = new()
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
{
{
new(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_IServiceCollection), "AddOpenApi"),
new("Microsoft.AspNetCore.OpenApi", "Microsoft.Extensions.DependencyInjection")
},
{
new(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Builder_WebApplication), "MapOpenApi"),
new("Microsoft.AspNetCore.OpenApi", "Microsoft.AspNetCore.Builder")
}
};

foreach (var diagnostic in context.Diagnostics)
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
{
var location = diagnostic.Location.SourceSpan;
var node = root.FindNode(location);
if (node == null)
{
return;
}
var methodName = node is IdentifierNameSyntax identifier ? identifier.Identifier.Text : null;
if (methodName == null)
{
return;
}

if (node.Parent is not MemberAccessExpressionSyntax)
{
return;
}

var symbol = semanticModel.GetSymbolInfo(((MemberAccessExpressionSyntax)node.Parent).Expression).Symbol;
var symbolType = symbol switch
{
IMethodSymbol methodSymbol => methodSymbol.ReturnType,
IPropertySymbol propertySymbol => propertySymbol.Type,
ILocalSymbol localSymbol => localSymbol.Type,
_ => null
};

if (symbolType == null)
{
return;
}

var targetThisAndExtensionMethod = new ThisAndExtensionMethod(symbolType, methodName);
if (_wellKnownExtensionMethodCache.TryGetValue(targetThisAndExtensionMethod, out var packageSourceAndNamespace))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a dictionary worthwhile for 2 members, or do we anticipate more in the near future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I anticipate that we'll have a larger collection here (see #56092). Whether or not Dictionary ends up being the right type for the complete set of extension methods remains to be seen but it seemed sufficient for now.

{
var position = diagnostic.Location.SourceSpan.Start;
var packageInstallData = new AspNetCoreInstallPackageData(
packageSource: null,
packageName: packageSourceAndNamespace.packageName,
packageVersionOpt: null,
packageNamespaceName: packageSourceAndNamespace.namespaceName);
var codeAction = await TryCreateCodeActionAsync(
context.Document,
position,
packageInstallData,
context.CancellationToken);

if (codeAction != null)
{
context.RegisterCodeFix(codeAction, diagnostic);
}
}

}
}

public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override ImmutableArray<string> FixableDiagnosticIds { get; } = ["CS1061"];
captainsafia marked this conversation as resolved.
Show resolved Hide resolved

internal virtual async Task<CodeAction?> TryCreateCodeActionAsync(
Document document,
int position,
AspNetCoreInstallPackageData packageInstallData,
CancellationToken cancellationToken)
{
var codeAction = await AspNetCoreAddPackageCodeAction.TryCreateCodeActionAsync(
document,
position,
packageInstallData,
cancellationToken);

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

namespace Microsoft.AspNetCore.Analyzers;

internal record struct PackageSourceAndNamespace(string packageName, string namespaceName);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Analyzers;

internal record struct ThisAndExtensionMethod(ITypeSymbol thisType, string extensionMethod);
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@
<ProjectReference Include="..\Analyzers\Microsoft.AspNetCore.App.Analyzers.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.App.Analyzers.Test" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.AddPackage;
using Moq;
using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpCodeFixVerifier<
Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.WebApplicationBuilderAnalyzer,
Microsoft.AspNetCore.Analyzers.Dependencies.AddPackagesTest.MockAddPackageFixer>;

namespace Microsoft.AspNetCore.Analyzers.Dependencies;

/// <remarks>
/// These tests don't assert the fix is applied, since it takes a dependency on the internal
/// VS-specific `PackageInstallerService`. However, the fixer is invoked in these codepaths
/// so we can validate that the symbol resolution and checks function correctly.
/// </remarks>
public class AddPackagesTest
{
[Fact]
public async Task CanFixMissingExtensionMethodForDI()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.{|CS1061:AddOpenApi|}();
";

// Assert
MockAddPackageFixer.Invoked = false;
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
await VerifyCS.VerifyCodeFixAsync(source, source);
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
Assert.True(MockAddPackageFixer.Invoked);
}

[Fact]
public async Task CanFixMissingExtensionMethodForBuilder()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var app = WebApplication.Create();

app.{|CS1061:MapOpenApi|}();
";

// Assert
MockAddPackageFixer.Invoked = false;
await VerifyCS.VerifyCodeFixAsync(source, source);
Assert.True(MockAddPackageFixer.Invoked);
}

public class MockAddPackageFixer : AddPackageFixer
{
internal static bool Invoked { get; set; }
internal override Task<CodeAction> TryCreateCodeActionAsync(
Document document,
int position,
AspNetCoreInstallPackageData packageInstallData,
CancellationToken cancellationToken)
{
Invoked = true;
Assert.Equal("Microsoft.AspNetCore.OpenApi", packageInstallData.PackageName);
return Task.FromResult<CodeAction>(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Http.Results" />
<Reference Include="Microsoft.AspNetCore.RateLimiting" />
<Reference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="All" />
<Reference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
<Reference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
</ItemGroup>
Expand Down
6 changes: 4 additions & 2 deletions src/Shared/RoslynUtils/WellKnownTypeData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ public enum WellKnownType
Microsoft_AspNetCore_Authorization_AuthorizeAttribute,
Microsoft_Extensions_DependencyInjection_PolicyServiceCollectionExtensions,
Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute,
Microsoft_AspNetCore_Authorization_AuthorizationOptions
Microsoft_AspNetCore_Authorization_AuthorizationOptions,
Microsoft_Extensions_DependencyInjection_IServiceCollection
}

public static string[] WellKnownTypeNames = new[]
Expand Down Expand Up @@ -222,6 +223,7 @@ public enum WellKnownType
"Microsoft.AspNetCore.Authorization.AuthorizeAttribute",
"Microsoft.Extensions.DependencyInjection.PolicyServiceCollectionExtensions",
"Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute",
"Microsoft.AspNetCore.Authorization.AuthorizationOptions"
"Microsoft.AspNetCore.Authorization.AuthorizationOptions",
"Microsoft.Extensions.DependencyInjection.IServiceCollection",
};
}
Loading