Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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.Tasks;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
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 sealed 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;
}
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 AspNetCoreAddPackageCodeAction.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"];
}
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,25 @@
// 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 Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Analyzers;

internal readonly struct ThisAndExtensionMethod(ITypeSymbol thisType, string extensionMethod)
{
public ITypeSymbol ThisType { get; } = thisType;
public string ExtensionMethod { get; } = extensionMethod;

public override bool Equals(object? obj)
{
if (obj is ThisAndExtensionMethod other)
{
return SymbolEqualityComparer.Default.Equals(ThisType, other.ThisType) &&
ExtensionMethod.Equals(other.ExtensionMethod, StringComparison.OrdinalIgnoreCase);
}
return false;
}

public override int GetHashCode() => HashCode.Combine(ThisType, 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>
<Compile Include="$(RepoRoot)\src\Shared\HashCode.cs" />
</ItemGroup>

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

using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpCodeFixVerifier<
Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.WebApplicationBuilderAnalyzer,
Microsoft.AspNetCore.Analyzers.Dependencies.AddPackageFixer>;

namespace Microsoft.AspNetCore.Analyzers.Dependencies;
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|}();
";
var fixedSource = @"
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();
";

// Assert
await VerifyCS.VerifyCodeFixAsync(source, fixedSource);
}

[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|}();
";
var fixedSource = @"
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var app = WebApplication.Create(args);

app.MapOpenApi();
";

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