forked from dotnet/aspnetcore
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add codefixer and completion provider to install OpenAPI package from…
… extension methods (dotnet#55963) * Add analyzer to install OpenAPI package from extension methods * Use Ordinal comparison for extension method names * Use records struct for comparison * Remove unneeded source include * Remove unrequired using * Check that code action was invoked in tests * More feedback * Add completion provider for extension methods in OpenAPI packages * Fix up reference and add comments * Fix up ThisAndExtensionMethod.GetHashCode
- Loading branch information
1 parent
333f1de
commit e558b05
Showing
11 changed files
with
564 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Dependencies/AddPackageFixer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
// 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.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; | ||
|
||
/// <summary> | ||
/// This fixer uses Roslyn's AspNetCoreAddPackageCodeAction to support providing a code fix for a missing | ||
/// package based on APIs defined in that package that are called in user code. This fixer is particularly | ||
/// helpful for providing guidance to users on how to add a missing package when they are using an extension | ||
/// method on well-known types like `IServiceCollection` and `IApplicationBuilder`. | ||
/// </summary> | ||
/// <remarks> | ||
/// This class is not sealed to support mocking of the virtual method `TryCreateCodeActionAsync` in unit tests. | ||
/// </remarks> | ||
[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); | ||
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 = ExtensionMethodsCache.ConstructFromWellKnownTypes(wellKnownTypes); | ||
|
||
// Diagnostics are already filtered by FixableDiagnosticIds values. | ||
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)) | ||
{ | ||
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; | ||
|
||
/// <example> | ||
/// 'IServiceCollection' does not contain a definition for 'AddOpenApi' and no accessible extension method 'AddOpenApi' accepting | ||
/// a first argument of type 'IServiceCollection' could be found (are you missing a using directive or an assembly reference?). | ||
/// </example> | ||
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; | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Dependencies/ExtensionMethodsCache.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
}; | ||
} | ||
} |
118 changes: 118 additions & 0 deletions
118
...work/AspNetCoreAnalyzers/src/CodeFixes/Dependencies/ExtensionMethodsCompletionProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// 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; | ||
|
||
/// <summary> | ||
/// This completion provider expands the completion list of target symbols defined in the | ||
/// ExtensionMethodsCache to include extension methods that can be invoked on the target | ||
/// type that are defined in auxillary packages. This completion provider is designed to be | ||
/// used in conjunction with the `AddPackageFixer` to recommend adding the missing packages | ||
/// extension methods are defined in. | ||
/// </summary> | ||
[ExportCompletionProvider(nameof(ExtensionMethodsCompletionProvider), LanguageNames.CSharp)] | ||
[Shared] | ||
public sealed class ExtensionMethodsCompletionProvider : CompletionProvider | ||
{ | ||
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 = ExtensionMethodsCache.ConstructFromWellKnownTypes(wellKnownTypes); | ||
|
||
// We find the nearest member access expression to the adjacent expression to resolve the | ||
// target type of the extension method that the user is invoking. For example, `app.` should | ||
// allow us to resolve to a `WebApplication` instance and `builder.Services.Add` should resolve | ||
// to an `IServiceCollection`. | ||
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; | ||
} | ||
|
||
// If the token that we are parsing is some sort of identifier, this indicates that the user | ||
// has triggered a completion with characters already inserted into the invocation (e.g. `builder.Services.Ad$$). | ||
// In this case, we only want to provide completions that match the characters that have been inserted. | ||
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)); | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Dependencies/PackageSourceAndNamespace.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
24 changes: 24 additions & 0 deletions
24
src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Dependencies/ThisAndExtensionMethod.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(SymbolEqualityComparer.Default.GetHashCode(ThisType), ExtensionMethod); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.