Skip to content
Merged
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
{
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);
if (root == null)
{
return;
}

if (semanticModel == null)
{
return;
}

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

Dictionary<ThisAndExtensionMethod, PackageSourceAndNamespace> _wellKnownExtensionMethodCache = new()
{
{
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)
{
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"];

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;
await VerifyCS.VerifyCodeFixAsync(source, source);
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",
};
}