-
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 8 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,113 @@ | ||
| // 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); | ||
| var wellKnownExtensionMethodCache = ExtensionMethodsClass.ConstructFromWellKnownTypes(wellKnownTypes); | ||
|
|
||
| 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,24 @@ | ||
| // 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 Microsoft.AspNetCore.Analyzers; | ||
| using Microsoft.AspNetCore.App.Analyzers.Infrastructure; | ||
|
|
||
| internal static class ExtensionMethodsCache | ||
| { | ||
| public static Dictionary<ThisAndExtensionMethod, PackageSourceAndNamespace> ConstructFromWellKnownTypes(WellKnownTypes wellKnownTypes) | ||
| { | ||
| return 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") | ||
| } | ||
| }; | ||
| } | ||
| } |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JamesNK Can I get your review on this given your experience implementing completion providers in route tooling? The goal is to add completions for extension methods defined in a separate package then provide an installation codefix for them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's been a minute since my head was in The Roslyn Zone. I'll do my best. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // 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.Composition; | ||
| using System.Linq; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.AspNetCore.App.Analyzers.Infrastructure; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.Completion; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
|
||
| namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; | ||
|
|
||
| [ExportCompletionProvider(nameof(ExtensionMethodsCompletionProvider), LanguageNames.CSharp)] | ||
| [Shared] | ||
| public sealed class ExtensionMethodsCompletionProvider : CompletionProvider | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a comment summarizing what the provider's deal is. Does it replace completion for known types or are values from the completion provider added to the auto-complete? Replacing auto-complete for some types (if that's how it works) feels pretty invasive. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The completion provider is additive (assuming I understand the intent of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if the value is already present in auto-complete? For example, someone might already have a reference to the OpenAPI package. Do they see the Do items added by this provider look the same or different to other items? Could I see a screenshot? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So excited! 🥳 |
||
| { | ||
| public override async Task ProvideCompletionsAsync(CompletionContext context) | ||
| { | ||
| if (!context.Document.SupportsSemanticModel) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
| if (root == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var span = context.CompletionListSpan; | ||
| var token = root.FindToken(span.Start); | ||
| if (token.Parent == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); | ||
| if (semanticModel == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var wellKnownTypes = WellKnownTypes.GetOrCreate(semanticModel.Compilation); | ||
| var wellKnownExtensionMethodCache = ExtensionMethodsClass.ConstructFromWellKnownTypes(wellKnownTypes); | ||
| var nearestMemberAccessExpression = FindNearestMemberAccessExpression(token.Parent); | ||
|
|
||
| if (nearestMemberAccessExpression is not null && nearestMemberAccessExpression is MemberAccessExpressionSyntax memberAccess) | ||
| { | ||
| var symbol = semanticModel.GetSymbolInfo(memberAccess.Expression); | ||
| var symbolType = symbol.Symbol switch | ||
| { | ||
| IMethodSymbol methodSymbol => methodSymbol.ReturnType, | ||
| IPropertySymbol propertySymbol => propertySymbol.Type, | ||
| ILocalSymbol localSymbol => localSymbol.Type, | ||
| _ => null | ||
| }; | ||
|
|
||
| var matchingExtensionMethods = wellKnownExtensionMethodCache.Where(pair => IsMatchingExtensionMethod(pair, symbolType, token)); | ||
| foreach (var item in matchingExtensionMethods) | ||
| { | ||
| context.CompletionListSpan = span; | ||
| context.AddItem(CompletionItem.Create( | ||
| displayText: item.Key.ExtensionMethod, | ||
| sortText: item.Key.ExtensionMethod, | ||
| filterText: item.Key.ExtensionMethod | ||
| )); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static SyntaxNode? FindNearestMemberAccessExpression(SyntaxNode? node) | ||
| { | ||
| var current = node; | ||
| while (current != null) | ||
| { | ||
| if (current?.IsKind(SyntaxKind.SimpleMemberAccessExpression) ?? false) | ||
| { | ||
| return current; | ||
| } | ||
|
|
||
| current = current?.Parent; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| private static bool IsMatchingExtensionMethod( | ||
| KeyValuePair<ThisAndExtensionMethod, PackageSourceAndNamespace> pair, | ||
| ISymbol? symbolType, | ||
| SyntaxToken token) | ||
| { | ||
| if (symbolType is null) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| var isIdentifierToken = token.IsKind(SyntaxKind.IdentifierName) || token.IsKind(SyntaxKind.IdentifierToken); | ||
| return SymbolEqualityComparer.Default.Equals(pair.Key.ThisType, symbolType) && | ||
| (!isIdentifierToken || pair.Key.ExtensionMethod.Contains(token.ValueText)); | ||
| } | ||
| } | ||
| 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
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.