diff --git a/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceCodeFixProvider.cs index 0e57a94e02e1f..a755f4763eef5 100644 --- a/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceCodeFixProvider.cs @@ -24,7 +24,4 @@ internal sealed class CSharpImplementInterfaceCodeFixProvider() public sealed override ImmutableArray FixableDiagnosticIds { get; } = [CS0535, CS0737, CS0738]; - - protected override bool IsTypeInInterfaceBaseList(TypeSyntax type) - => type.Parent is BaseTypeSyntax { Parent: BaseListSyntax } baseTypeParent && baseTypeParent.Type == type; } diff --git a/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceService.cs b/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceService.cs index 23efc1a7f4f46..b9fd6197a06e4 100644 --- a/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceService.cs +++ b/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceService.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.ImplementInterface; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface; @@ -23,7 +24,7 @@ namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface; [ExportLanguageService(typeof(IImplementInterfaceService), LanguageNames.CSharp), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class CSharpImplementInterfaceService() : AbstractImplementInterfaceService +internal sealed class CSharpImplementInterfaceService() : AbstractImplementInterfaceService { protected override ISyntaxFormatting SyntaxFormatting => CSharpSyntaxFormatting.Instance; @@ -37,6 +38,18 @@ protected override string ToDisplayString(IMethodSymbol disposeImplMethod, Symbo protected override bool AllowDelegateAndEnumConstraints(ParseOptions options) => options.LanguageVersion() >= LanguageVersion.CSharp7_3; + protected override bool IsTypeInInterfaceBaseList(SyntaxNode? type) + => type?.Parent is BaseTypeSyntax { Parent: BaseListSyntax } baseTypeParent && baseTypeParent.Type == type; + + protected override void AddInterfaceTypes(TypeDeclarationSyntax typeDeclaration, ArrayBuilder result) + { + if (typeDeclaration.BaseList != null) + { + foreach (var baseType in typeDeclaration.BaseList.Types) + result.Add(baseType.Type); + } + } + protected override bool TryInitializeState( Document document, SemanticModel model, SyntaxNode node, CancellationToken cancellationToken, [NotNullWhen(true)] out SyntaxNode? classOrStructDecl, diff --git a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems index 2fb3045391263..26423cafb96a6 100644 --- a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems +++ b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems @@ -43,8 +43,8 @@ - - + + diff --git a/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceTests.cs b/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.cs similarity index 99% rename from src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceTests.cs rename to src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.cs index ec10365e15a3c..e7e6d5dbf148c 100644 --- a/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceTests.cs +++ b/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.cs @@ -23,7 +23,7 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ImplementInterface; CSharpImplementInterfaceCodeFixProvider>; [Trait(Traits.Feature, Traits.Features.CodeActionsImplementInterface)] -public sealed class ImplementInterfaceTests +public sealed class ImplementInterfaceCodeFixTests { private readonly NamingStylesTestOptionSets _options = new(LanguageNames.CSharp); diff --git a/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceTests_FixAllTests.cs b/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests_FixAllTests.cs similarity index 99% rename from src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceTests_FixAllTests.cs rename to src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests_FixAllTests.cs index 761d785b2eaf6..afc9042b8496c 100644 --- a/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceTests_FixAllTests.cs +++ b/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests_FixAllTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ImplementInterface; EmptyDiagnosticAnalyzer, CSharpImplementInterfaceCodeFixProvider>; -public sealed class ImplementInterfaceTests_FixAllTests +public sealed class ImplementInterfaceCodeFixTests_FixAllTests { #region "Fix all occurrences tests" diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs index da11cdb7fca36..7fffec0d40da3 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs @@ -2,28 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.ImplementType; -using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.ImplementInterface; -using static ImplementHelpers; - internal abstract class AbstractImplementInterfaceCodeFixProvider : CodeFixProvider where TTypeSyntax : SyntaxNode { - protected abstract bool IsTypeInInterfaceBaseList(TTypeSyntax type); - public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -39,173 +27,11 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) if (!token.Span.IntersectsWith(span)) return; - var options = await document.GetImplementTypeOptionsAsync(cancellationToken).ConfigureAwait(false); - - foreach (var type in token.Parent.GetAncestorsOrThis()) - { - if (this.IsTypeInInterfaceBaseList(type)) - { - var service = document.GetRequiredLanguageService(); - - var info = await service.AnalyzeAsync( - document, type, cancellationToken).ConfigureAwait(false); - if (info is not null) - { - using var _ = ArrayBuilder.GetInstance(out var codeActions); - await foreach (var implementOptions in GetImplementOptionsAsync(document, info, cancellationToken)) - { - var title = GetTitle(implementOptions); - var equivalenceKey = GetEquivalenceKey(info, implementOptions); - codeActions.Add(CodeAction.Create( - title, - cancellationToken => service.ImplementInterfaceAsync( - document, info, options, implementOptions, cancellationToken), - equivalenceKey)); - } - - context.RegisterFixes(codeActions, context.Diagnostics); - } - - break; - } - } - } - - private static string GetTitle(ImplementInterfaceConfiguration options) - { - if (options.ImplementDisposePattern) - { - return options.Explicitly - ? CodeFixesResources.Implement_interface_explicitly_with_Dispose_pattern - : CodeFixesResources.Implement_interface_with_Dispose_pattern; - } - else if (options.Explicitly) - { - return options.OnlyRemaining - ? CodeFixesResources.Implement_remaining_members_explicitly - : CodeFixesResources.Implement_all_members_explicitly; - } - else if (options.Abstractly) - { - return CodeFixesResources.Implement_interface_abstractly; - } - else if (options.ThroughMember != null) - { - return string.Format(CodeFixesResources.Implement_interface_through_0, options.ThroughMember.Name); - } - else - { - return CodeFixesResources.Implement_interface; - } - } - - private static string GetEquivalenceKey( - ImplementInterfaceInfo state, - ImplementInterfaceConfiguration options) - { - var interfaceType = state.InterfaceTypes.First(); - var typeName = interfaceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - // Legacy part of the equivalence key. Kept the same to avoid test churn. - var codeActionTypeName = options.ImplementDisposePattern - ? "Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceWithDisposePatternCodeAction" - : "Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceCodeAction"; - - // Consider code actions equivalent if they correspond to the same interface being implemented elsewhere - // in the same manner. Note: 'implement through member' means implementing the same interface through - // an applicable member with the same name in the destination. - return options.Explicitly.ToString() + ";" + - options.Abstractly.ToString() + ";" + - options.OnlyRemaining.ToString() + ":" + - typeName + ";" + - codeActionTypeName + ";" + - options.ThroughMember?.Name; - } - - private static async IAsyncEnumerable GetImplementOptionsAsync( - Document document, ImplementInterfaceInfo state, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - var supportsImplicitImplementationOfNonPublicInterfaceMembers = syntaxFacts.SupportsImplicitImplementationOfNonPublicInterfaceMembers(document.Project.ParseOptions!); - if (state.MembersWithoutExplicitOrImplicitImplementationWhichCanBeImplicitlyImplemented.Length > 0) - { - var totalMemberCount = 0; - var inaccessibleMemberCount = 0; - - foreach (var (_, members) in state.MembersWithoutExplicitOrImplicitImplementationWhichCanBeImplicitlyImplemented) - { - foreach (var member in members) - { - totalMemberCount++; - - if (ContainsTypeLessAccessibleThan(member, state.ClassOrStructType, supportsImplicitImplementationOfNonPublicInterfaceMembers)) - inaccessibleMemberCount++; - } - } - - // If all members to implement are inaccessible, then "Implement interface" codeaction - // will be the same as "Implement interface explicitly", so there is no point in having both of them - if (totalMemberCount != inaccessibleMemberCount) - yield return new() { OnlyRemaining = true }; - - if (ShouldImplementDisposePattern(compilation, state, explicitly: false)) - yield return new() { OnlyRemaining = true, ImplementDisposePattern = true, }; - - var delegatableMembers = GetDelegatableMembers(document, state, cancellationToken); - foreach (var member in delegatableMembers) - yield return new() { ThroughMember = member }; + var type = token.Parent.GetAncestorsOrThis().LastOrDefault(); - if (state.ClassOrStructType.IsAbstract) - yield return new() { OnlyRemaining = true, Abstractly = true }; - } - - if (state.MembersWithoutExplicitImplementation.Length > 0) - { - yield return new() { Explicitly = true }; - - if (ShouldImplementDisposePattern(compilation, state, explicitly: true)) - yield return new() { ImplementDisposePattern = true, Explicitly = true }; - } - - if (AnyImplementedImplicitly(state)) - yield return new() { OnlyRemaining = true, Explicitly = true }; - } - - private static bool AnyImplementedImplicitly(ImplementInterfaceInfo state) - { - if (state.MembersWithoutExplicitOrImplicitImplementation.Length != state.MembersWithoutExplicitImplementation.Length) - { - return true; - } - - for (var i = 0; i < state.MembersWithoutExplicitOrImplicitImplementation.Length; i++) - { - var (typeA, membersA) = state.MembersWithoutExplicitOrImplicitImplementation[i]; - var (typeB, membersB) = state.MembersWithoutExplicitImplementation[i]; - if (!typeA.Equals(typeB)) - { - return true; - } - - if (!membersA.SequenceEqual(membersB)) - { - return true; - } - } - - return false; - } - - private static ImmutableArray GetDelegatableMembers( - Document document, ImplementInterfaceInfo state, CancellationToken cancellationToken) - { - var firstInterfaceType = state.InterfaceTypes.First(); + var service = document.GetRequiredLanguageService(); + var codeActions = await service.GetCodeActionsAsync(document, type, cancellationToken).ConfigureAwait(false); - return ImplementHelpers.GetDelegatableMembers( - document, - state.ClassOrStructType, - t => t.GetAllInterfacesIncludingThis().Contains(firstInterfaceType), - cancellationToken); + context.RegisterFixes(codeActions, context.Diagnostics); } } diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.State.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.State.cs index 282036cd5b437..cff4072d33c05 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.State.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.State.cs @@ -9,7 +9,7 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; -internal abstract partial class AbstractImplementInterfaceService +internal abstract partial class AbstractImplementInterfaceService { internal sealed class State( Document document, @@ -40,7 +40,7 @@ internal sealed class State( public ImmutableArray<(INamedTypeSymbol type, ImmutableArray members)> MembersWithoutExplicitImplementation => Info.MembersWithoutExplicitImplementation; public static State? Generate( - AbstractImplementInterfaceService service, + AbstractImplementInterfaceService service, Document document, SemanticModel model, SyntaxNode interfaceNode, diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs index 9ccbb6fe9cec9..0d85020a1be91 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs @@ -3,10 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.ImplementType; @@ -19,7 +23,8 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; using static ImplementHelpers; -internal abstract partial class AbstractImplementInterfaceService() : IImplementInterfaceService +internal abstract partial class AbstractImplementInterfaceService : IImplementInterfaceService + where TTypeDeclarationSyntax : SyntaxNode { protected const string DisposingName = "disposing"; @@ -39,6 +44,18 @@ protected abstract bool TryInitializeState(Document document, SemanticModel mode protected abstract SyntaxNode AddCommentInsideIfStatement(SyntaxNode ifDisposingStatement, SyntaxTriviaList trivia); protected abstract SyntaxNode CreateFinalizer(SyntaxGenerator generator, INamedTypeSymbol classType, string disposeMethodDisplayString); + protected abstract bool IsTypeInInterfaceBaseList([NotNullWhen(true)] SyntaxNode? type); + protected abstract void AddInterfaceTypes(TTypeDeclarationSyntax typeDeclaration, ArrayBuilder result); + + public ImmutableArray GetInterfaceTypes(SyntaxNode typeDeclaration) + { + using var _ = ArrayBuilder.GetInstance(out var result); + if (typeDeclaration is TTypeDeclarationSyntax typeSyntax) + AddInterfaceTypes(typeSyntax, result); + + return result.ToImmutableAndClear(); + } + public async Task ImplementInterfaceAsync( Document document, ImplementTypeOptions options, SyntaxNode node, CancellationToken cancellationToken) { @@ -59,7 +76,7 @@ public async Task ImplementInterfaceAsync( } } - public async Task AnalyzeAsync(Document document, SyntaxNode interfaceType, CancellationToken cancellationToken) + private async Task AnalyzeAsync(Document document, SyntaxNode interfaceType, CancellationToken cancellationToken) { var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); return State.Generate(this, document, model, interfaceType, cancellationToken)?.Info; @@ -88,7 +105,7 @@ protected SyntaxTriviaList CreateCommentTrivia( return [.. trivia]; } - public async Task ImplementInterfaceAsync( + private async Task ImplementInterfaceAsync( Document document, ImplementInterfaceInfo info, ImplementTypeOptions options, @@ -126,4 +143,169 @@ public ImmutableArray ImplementInterfaceMember( return implementedMembers; } + + public async Task> GetCodeActionsAsync(Document document, SyntaxNode? interfaceType, CancellationToken cancellationToken) + { + var options = await document.GetImplementTypeOptionsAsync(cancellationToken).ConfigureAwait(false); + + if (!this.IsTypeInInterfaceBaseList(interfaceType)) + return []; + + var info = await this.AnalyzeAsync( + document, interfaceType, cancellationToken).ConfigureAwait(false); + if (info is null) + return []; + + using var _ = ArrayBuilder.GetInstance(out var codeActions); + await foreach (var implementOptions in GetImplementOptionsAsync(document, info, cancellationToken)) + { + var title = GetTitle(implementOptions); + var equivalenceKey = GetEquivalenceKey(info, implementOptions); + codeActions.Add(CodeAction.Create( + title, + cancellationToken => this.ImplementInterfaceAsync( + document, info, options, implementOptions, cancellationToken), + equivalenceKey)); + } + + return codeActions.ToImmutableAndClear(); + } + + private static async IAsyncEnumerable GetImplementOptionsAsync( + Document document, ImplementInterfaceInfo state, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var supportsImplicitImplementationOfNonPublicInterfaceMembers = syntaxFacts.SupportsImplicitImplementationOfNonPublicInterfaceMembers(document.Project.ParseOptions!); + if (state.MembersWithoutExplicitOrImplicitImplementationWhichCanBeImplicitlyImplemented.Length > 0) + { + var totalMemberCount = 0; + var inaccessibleMemberCount = 0; + + foreach (var (_, members) in state.MembersWithoutExplicitOrImplicitImplementationWhichCanBeImplicitlyImplemented) + { + foreach (var member in members) + { + totalMemberCount++; + + if (ContainsTypeLessAccessibleThan(member, state.ClassOrStructType, supportsImplicitImplementationOfNonPublicInterfaceMembers)) + inaccessibleMemberCount++; + } + } + + // If all members to implement are inaccessible, then "Implement interface" codeaction + // will be the same as "Implement interface explicitly", so there is no point in having both of them + if (totalMemberCount != inaccessibleMemberCount) + yield return new() { OnlyRemaining = true }; + + if (ShouldImplementDisposePattern(compilation, state, explicitly: false)) + yield return new() { OnlyRemaining = true, ImplementDisposePattern = true, }; + + var delegatableMembers = GetDelegatableMembers(document, state, cancellationToken); + foreach (var member in delegatableMembers) + yield return new() { ThroughMember = member }; + + if (state.ClassOrStructType.IsAbstract) + yield return new() { OnlyRemaining = true, Abstractly = true }; + } + + if (state.MembersWithoutExplicitImplementation.Length > 0) + { + yield return new() { Explicitly = true }; + + if (ShouldImplementDisposePattern(compilation, state, explicitly: true)) + yield return new() { ImplementDisposePattern = true, Explicitly = true }; + } + + if (AnyImplementedImplicitly(state)) + yield return new() { OnlyRemaining = true, Explicitly = true }; + } + + private static string GetTitle(ImplementInterfaceConfiguration options) + { + if (options.ImplementDisposePattern) + { + return options.Explicitly + ? CodeFixesResources.Implement_interface_explicitly_with_Dispose_pattern + : CodeFixesResources.Implement_interface_with_Dispose_pattern; + } + else if (options.Explicitly) + { + return options.OnlyRemaining + ? CodeFixesResources.Implement_remaining_members_explicitly + : CodeFixesResources.Implement_all_members_explicitly; + } + else if (options.Abstractly) + { + return CodeFixesResources.Implement_interface_abstractly; + } + else if (options.ThroughMember != null) + { + return string.Format(CodeFixesResources.Implement_interface_through_0, options.ThroughMember.Name); + } + else + { + return CodeFixesResources.Implement_interface; + } + } + + private static string GetEquivalenceKey( + ImplementInterfaceInfo state, + ImplementInterfaceConfiguration options) + { + var interfaceType = state.InterfaceTypes.First(); + var typeName = interfaceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + // Legacy part of the equivalence key. Kept the same to avoid test churn. + var codeActionTypeName = options.ImplementDisposePattern + ? "Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceWithDisposePatternCodeAction" + : "Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceCodeAction"; + + // Consider code actions equivalent if they correspond to the same interface being implemented elsewhere + // in the same manner. Note: 'implement through member' means implementing the same interface through + // an applicable member with the same name in the destination. + return options.Explicitly.ToString() + ";" + + options.Abstractly.ToString() + ";" + + options.OnlyRemaining.ToString() + ":" + + typeName + ";" + + codeActionTypeName + ";" + + options.ThroughMember?.Name; + } + + private static bool AnyImplementedImplicitly(ImplementInterfaceInfo state) + { + if (state.MembersWithoutExplicitOrImplicitImplementation.Length != state.MembersWithoutExplicitImplementation.Length) + { + return true; + } + + for (var i = 0; i < state.MembersWithoutExplicitOrImplicitImplementation.Length; i++) + { + var (typeA, membersA) = state.MembersWithoutExplicitOrImplicitImplementation[i]; + var (typeB, membersB) = state.MembersWithoutExplicitImplementation[i]; + if (!typeA.Equals(typeB)) + { + return true; + } + + if (!membersA.SequenceEqual(membersB)) + { + return true; + } + } + + return false; + } + + private static ImmutableArray GetDelegatableMembers( + Document document, ImplementInterfaceInfo state, CancellationToken cancellationToken) + { + var firstInterfaceType = state.InterfaceTypes.First(); + + return ImplementHelpers.GetDelegatableMembers( + document, + state.ClassOrStructType, + t => t.GetAllInterfacesIncludingThis().Contains(firstInterfaceType), + cancellationToken); + } } diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs index 55b0b6d99cd07..32df9beb85355 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.ImplementType; @@ -23,15 +24,6 @@ internal interface IImplementInterfaceService : ILanguageService { Task ImplementInterfaceAsync(Document document, ImplementTypeOptions options, SyntaxNode node, CancellationToken cancellationToken); - Task AnalyzeAsync(Document document, SyntaxNode interfaceType, CancellationToken cancellationToken); - - Task ImplementInterfaceAsync( - Document document, - ImplementInterfaceInfo info, - ImplementTypeOptions options, - ImplementInterfaceConfiguration configuration, - CancellationToken cancellationToken); - /// /// Produces the symbol that implements that provided within the corresponding /// , based on the provided and @@ -44,4 +36,9 @@ ImmutableArray ImplementInterfaceMember( ImplementInterfaceConfiguration configuration, Compilation compilation, ISymbol interfaceMember); + + Task> GetCodeActionsAsync( + Document document, SyntaxNode? interfaceType, CancellationToken cancellationToken); + + ImmutableArray GetInterfaceTypes(SyntaxNode typeDeclaration); } diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator.cs index fc5078f1ea994..b0b61f3fdbcd7 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator.cs @@ -22,12 +22,12 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; using static ImplementHelpers; -internal abstract partial class AbstractImplementInterfaceService +internal abstract partial class AbstractImplementInterfaceService { private sealed partial class ImplementInterfaceGenerator { private readonly Document Document; - private readonly AbstractImplementInterfaceService Service; + private readonly AbstractImplementInterfaceService Service; private readonly ImplementInterfaceInfo State; private readonly ImplementTypeOptions Options; @@ -40,7 +40,7 @@ private sealed partial class ImplementInterfaceGenerator private ISymbol? ThroughMember => Configuration.ThroughMember; internal ImplementInterfaceGenerator( - AbstractImplementInterfaceService service, + AbstractImplementInterfaceService service, Document document, ImplementInterfaceInfo state, ImplementTypeOptions options, diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Conflicts.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Conflicts.cs index fb554b99f4494..f9ce33830bc74 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Conflicts.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Conflicts.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; -internal abstract partial class AbstractImplementInterfaceService +internal abstract partial class AbstractImplementInterfaceService { private sealed partial class ImplementInterfaceGenerator { diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_DisposePattern.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_DisposePattern.cs index 0887f5f52ce86..4bc54edb2fc92 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_DisposePattern.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_DisposePattern.cs @@ -22,7 +22,7 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; using static ImplementHelpers; -internal abstract partial class AbstractImplementInterfaceService +internal abstract partial class AbstractImplementInterfaceService { private sealed partial class ImplementInterfaceGenerator { diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Method.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Method.cs index 48a343cdcfe7c..a08aa2daff3e5 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Method.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Method.cs @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; -internal abstract partial class AbstractImplementInterfaceService +internal abstract partial class AbstractImplementInterfaceService { private sealed partial class ImplementInterfaceGenerator { diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Property.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Property.cs index b31a55c26bbe1..75b8cd535f3c4 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Property.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/ImplementInterfaceGenerator_Property.cs @@ -16,7 +16,7 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; -internal abstract partial class AbstractImplementInterfaceService +internal abstract partial class AbstractImplementInterfaceService { private sealed partial class ImplementInterfaceGenerator { diff --git a/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceCodeFixProvider.vb b/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceCodeFixProvider.vb index 8aa097942b9b2..634e140216ff4 100644 --- a/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceCodeFixProvider.vb +++ b/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceCodeFixProvider.vb @@ -23,9 +23,5 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.ImplementInterface End Sub Public Overrides ReadOnly Property FixableDiagnosticIds As ImmutableArray(Of String) = ImmutableArray.Create(BC30149) - - Protected Overrides Function IsTypeInInterfaceBaseList(type As TypeSyntax) As Boolean - Return TypeOf type.Parent Is ImplementsStatementSyntax - End Function End Class End Namespace diff --git a/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb b/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb index 161a9857bd4db..eb0d4a2a879bc 100644 --- a/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb +++ b/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb @@ -10,14 +10,15 @@ Imports Microsoft.CodeAnalysis.Editing Imports Microsoft.CodeAnalysis.Formatting Imports Microsoft.CodeAnalysis.Host.Mef Imports Microsoft.CodeAnalysis.ImplementInterface +Imports Microsoft.CodeAnalysis.PooledObjects Imports Microsoft.CodeAnalysis.VisualBasic.CodeGeneration Imports Microsoft.CodeAnalysis.VisualBasic.Formatting Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.ImplementInterface - Partial Friend Class VisualBasicImplementInterfaceService - Inherits AbstractImplementInterfaceService + Partial Friend NotInheritable Class VisualBasicImplementInterfaceService + Inherits AbstractImplementInterfaceService(Of TypeBlockSyntax) @@ -39,6 +40,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.ImplementInterface Return False End Function + Protected Overrides Function IsTypeInInterfaceBaseList(type As SyntaxNode) As Boolean + Return TypeOf type?.Parent Is ImplementsStatementSyntax + End Function + + Protected Overrides Sub AddInterfaceTypes(typeDeclaration As TypeBlockSyntax, result As ArrayBuilder(Of SyntaxNode)) + For Each implementsStatement In typeDeclaration.Implements + For Each interfaceType In implementsStatement.Types + result.Add(interfaceType) + Next + Next + End Sub + Protected Overrides Function TryInitializeState( document As Document, model As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken, ByRef classOrStructDecl As SyntaxNode, ByRef classOrStructType As INamedTypeSymbol, diff --git a/src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceTests.vb b/src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.vb similarity index 99% rename from src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceTests.vb rename to src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.vb index 9b9f0deaa814b..583aba1015858 100644 --- a/src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceTests.vb +++ b/src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.vb @@ -10,7 +10,7 @@ Imports Microsoft.CodeAnalysis.VisualBasic.ImplementInterface Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.ImplementInterface - Partial Public Class ImplementInterfaceTests + Partial Public Class ImplementInterfaceCodeFixTests Inherits AbstractVisualBasicDiagnosticProviderBasedUserDiagnosticTest_NoEditor Friend Overrides Function CreateDiagnosticProviderAndFixer(workspace As Workspace) As (DiagnosticAnalyzer, CodeFixProvider) diff --git a/src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceTests_FixAllTests.vb b/src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceCodeFixTests_FixAllTests.vb similarity index 99% rename from src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceTests_FixAllTests.vb rename to src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceCodeFixTests_FixAllTests.vb index 46138758ecf48..4f5fd12b08d78 100644 --- a/src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceTests_FixAllTests.vb +++ b/src/Analyzers/VisualBasic/Tests/ImplementInterface/ImplementInterfaceCodeFixTests_FixAllTests.vb @@ -3,7 +3,7 @@ ' See the LICENSE file in the project root for more information. Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.ImplementInterface - Partial Public Class ImplementInterfaceTests + Partial Public Class ImplementInterfaceCodeFixTests diff --git a/src/Analyzers/VisualBasic/Tests/VisualBasicAnalyzers.UnitTests.projitems b/src/Analyzers/VisualBasic/Tests/VisualBasicAnalyzers.UnitTests.projitems index 9f5434bfc464c..fcfafc799ce1a 100644 --- a/src/Analyzers/VisualBasic/Tests/VisualBasicAnalyzers.UnitTests.projitems +++ b/src/Analyzers/VisualBasic/Tests/VisualBasicAnalyzers.UnitTests.projitems @@ -29,8 +29,8 @@ - - + + diff --git a/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs b/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs new file mode 100644 index 0000000000000..820dbc26a4f4a --- /dev/null +++ b/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.ImplementInterface; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.ImplementInterface; + +using VerifyCS = CSharpCodeRefactoringVerifier< + ImplementInterfaceCodeRefactoringProvider>; + +[UseExportProvider] +[Trait(Traits.Feature, Traits.Features.CodeActionsImplementInterface)] +public sealed class ImplementInterfaceCodeRefactoringTests +{ + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/78294")] + public Task TestInBody() + => VerifyCS.VerifyRefactoringAsync(""" + interface IGoo + { + void Goo(); + } + + class C : {|CS0535:IGoo|} + { + $$ + } + """, """ + interface IGoo + { + void Goo(); + } + + class C : IGoo + { + public void Goo() + { + throw new System.NotImplementedException(); + } + } + """); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/78294")] + public Task TestNotOnInterfaceInBody() + => VerifyCS.VerifyRefactoringAsync(""" + interface IGoo + { + void Goo(); + } + + interface IBar : IGoo + { + $$ + } + """); +} diff --git a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs index c6e7daf484e8d..47c3cd2a86a10 100644 --- a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs +++ b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs @@ -47,6 +47,7 @@ internal static class PredefinedCodeRefactoringProviderNames public const string GenerateConstructorFromMembers = "Generate Constructor From Members Code Action Provider"; public const string GenerateEqualsAndGetHashCodeFromMembers = "Generate Equals and GetHashCode Code Action Provider"; public const string GenerateOverrides = "Generate Overrides Code Action Provider"; + public const string ImplementInterface = nameof(ImplementInterface); public const string ImplementInterfaceExplicitly = nameof(ImplementInterfaceExplicitly); public const string ImplementInterfaceImplicitly = nameof(ImplementInterfaceImplicitly); public const string InitializeMemberFromParameter = nameof(InitializeMemberFromParameter); diff --git a/src/Features/Core/Portable/ImplementInterface/ImplementInterfaceCodeRefactoringProvider.cs b/src/Features/Core/Portable/ImplementInterface/ImplementInterfaceCodeRefactoringProvider.cs new file mode 100644 index 0000000000000..c75c4a473a419 --- /dev/null +++ b/src/Features/Core/Portable/ImplementInterface/ImplementInterfaceCodeRefactoringProvider.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.ImplementInterface; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, + Name = PredefinedCodeRefactoringProviderNames.ImplementInterface), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ImplementInterfaceCodeRefactoringProvider() : CodeRefactoringProvider +{ + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, textSpan, cancellationToken) = context; + + var helpers = document.GetRequiredLanguageService(); + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + // We offer the refactoring when the user is between any members of a class/struct and are on a blank line. + if (!helpers.IsBetweenTypeMembers(sourceText, root, textSpan.Start, out var typeDeclaration)) + return; + + var service = document.GetRequiredLanguageService(); + using var allCodeActions = TemporaryArray.Empty; + + foreach (var typeNode in service.GetInterfaceTypes(typeDeclaration)) + { + var codeActions = await service.GetCodeActionsAsync( + document, typeNode, cancellationToken).ConfigureAwait(false); + + allCodeActions.AddRange(codeActions); + } + + context.RegisterRefactorings(allCodeActions.ToImmutableAndClear(), textSpan); + } +} diff --git a/src/Features/DiagnosticsTestUtilities/CodeActions/VisualBasicCodeRefactoringVerifier`1.cs b/src/Features/DiagnosticsTestUtilities/CodeActions/VisualBasicCodeRefactoringVerifier`1.cs index 1a31a8b1f8b45..d45791c0dbb11 100644 --- a/src/Features/DiagnosticsTestUtilities/CodeActions/VisualBasicCodeRefactoringVerifier`1.cs +++ b/src/Features/DiagnosticsTestUtilities/CodeActions/VisualBasicCodeRefactoringVerifier`1.cs @@ -12,6 +12,13 @@ namespace Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; public static partial class VisualBasicCodeRefactoringVerifier where TCodeRefactoring : CodeRefactoringProvider, new() { + /// + public static Task VerifyRefactoringAsync( + string source) + { + return VerifyRefactoringAsync(source, DiagnosticResult.EmptyDiagnosticResults, source); + } + /// public static Task VerifyRefactoringAsync(string source, string fixedSource) { diff --git a/src/Features/VisualBasicTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.vb b/src/Features/VisualBasicTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.vb new file mode 100644 index 0000000000000..6d26e13134ed7 --- /dev/null +++ b/src/Features/VisualBasicTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.vb @@ -0,0 +1,55 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports VerifyVb = Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions.VisualBasicCodeRefactoringVerifier(Of + Microsoft.CodeAnalysis.ImplementInterface.ImplementInterfaceCodeRefactoringProvider) + +Namespace Microsoft.CodeAnalysis.VisualBasic.UnitTests.ImplementInterface + + + Public NotInheritable Class ImplementInterfaceCodeRefactoringTests + + + Public Function TestInBody() As Task + Return VerifyVb.VerifyRefactoringAsync(" +interface IGoo + sub Goo() +end interface + +class C + implements {|BC30149:IGoo|} + + $$ +end class + ", " +interface IGoo + sub Goo() +end interface + +class C + implements IGoo + + Public Sub Goo() Implements IGoo.Goo + Throw New System.NotImplementedException() + End Sub +end class + ") + End Function + + + Public Function TestNotOnInterfaceInBody() As Task + Return VerifyVb.VerifyRefactoringAsync(" +interface IGoo + sub Goo() +end interface + +interface IBar + inherits IGoo + + $$ +end interface + ") + End Function + End Class +End Namespace