-
Couldn't load subscription status.
- Fork 10.5k
Add codefixer and completion provider to install OpenAPI package from extension methods #55963
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
Changes from 7 commits
411d738
ab4bbe6
a3531f0
1a8c286
f92547b
63e2ba0
87be404
a4797f8
b1a4ecc
e9f6fba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
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); | ||
| if (root == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); | ||
| if (semanticModel == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var wellKnownTypes = WellKnownTypes.GetOrCreate(semanticModel.Compilation); | ||
|
|
||
| Dictionary<ThisAndExtensionMethod, PackageSourceAndNamespace> _wellKnownExtensionMethodCache = new() | ||
captainsafia marked this conversation as resolved.
Outdated
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)) | ||
|
||
| { | ||
| 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,24 @@ | ||
| // 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 readonly struct ThisAndExtensionMethod(ITypeSymbol thisType, string extensionMethod) | ||
| { | ||
| public ITypeSymbol ThisType { get; } = thisType; | ||
| public string ExtensionMethod { get; } = extensionMethod; | ||
|
|
||
| public override bool Equals(object obj) | ||
| { | ||
| return obj is ThisAndExtensionMethod other && | ||
| SymbolEqualityComparer.Default.Equals(ThisType, other.ThisType) && | ||
| ExtensionMethod == other.ExtensionMethod; | ||
| } | ||
|
|
||
| public override int GetHashCode() | ||
| { | ||
| return HashCode.Combine(ThisType, ExtensionMethod); | ||
captainsafia marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| // 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 | ||
| { | ||
| /// <remarks> | ||
| /// This static property allows us to verify that the fixer was | ||
| /// able to successfully resolve the symbol and call into the | ||
| /// package install APIs. This is a workaround for the fact that | ||
| /// the package install APIs are not readily mockable. Note: this | ||
| /// is not intended for use across test classes. | ||
| /// </remarks> | ||
| 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); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.