From 5c849d2dd08d041a4be8d88b0620d9b2a333d34f Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 14 Jul 2025 13:36:21 -0700 Subject: [PATCH 1/6] Refactor implement interface to push code into service --- ...CSharpImplementInterfaceCodeFixProvider.cs | 3 - .../CSharpImplementInterfaceService.cs | 3 + ...stractImplementInterfaceCodeFixProvider.cs | 172 +---------------- .../AbstractImplementInterfaceService.cs | 173 +++++++++++++++++- .../IImplementInterfaceService.cs | 4 + ...lBasicImplementInterfaceCodeFixProvider.vb | 4 - .../VisualBasicImplementInterfaceService.vb | 6 +- 7 files changed, 188 insertions(+), 177 deletions(-) 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..714cf151591f2 100644 --- a/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceService.cs +++ b/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceService.cs @@ -37,6 +37,9 @@ 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 bool TryInitializeState( Document document, SemanticModel model, SyntaxNode node, CancellationToken cancellationToken, [NotNullWhen(true)] out SyntaxNode? classOrStructDecl, diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs index da11cdb7fca36..a165be3c57854 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs @@ -22,8 +22,6 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; internal abstract class AbstractImplementInterfaceCodeFixProvider : CodeFixProvider where TTypeSyntax : SyntaxNode { - protected abstract bool IsTypeInInterfaceBaseList(TTypeSyntax type); - public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -39,173 +37,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++; + var type = token.Parent.GetAncestorsOrThis().LastOrDefault(); - 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 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.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs index 9ccbb6fe9cec9..57a856052a176 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,7 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; using static ImplementHelpers; -internal abstract partial class AbstractImplementInterfaceService() : IImplementInterfaceService +internal abstract partial class AbstractImplementInterfaceService : IImplementInterfaceService { protected const string DisposingName = "disposing"; @@ -39,6 +43,8 @@ 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); + public async Task ImplementInterfaceAsync( Document document, ImplementTypeOptions options, SyntaxNode node, CancellationToken cancellationToken) { @@ -126,4 +132,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..fcd1024695861 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; @@ -44,4 +45,7 @@ ImmutableArray ImplementInterfaceMember( ImplementInterfaceConfiguration configuration, Compilation compilation, ISymbol interfaceMember); + + Task> GetCodeActionsAsync( + Document document, SyntaxNode? interfaceType, CancellationToken cancellationToken); } 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..ca90aa58e1fe6 100644 --- a/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb +++ b/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb @@ -16,7 +16,7 @@ Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.ImplementInterface - Partial Friend Class VisualBasicImplementInterfaceService + Partial Friend NotInheritable Class VisualBasicImplementInterfaceService Inherits AbstractImplementInterfaceService @@ -39,6 +39,10 @@ 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 Function TryInitializeState( document As Document, model As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken, ByRef classOrStructDecl As SyntaxNode, ByRef classOrStructType As INamedTypeSymbol, From 9d2672f3fa6fd0999e4c02d52586865319af1bb7 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 14 Jul 2025 13:46:24 -0700 Subject: [PATCH 2/6] Tests --- .../Tests/CSharpAnalyzers.UnitTests.projitems | 4 +- ...s.cs => ImplementInterfaceCodeFixTests.cs} | 2 +- ...ementInterfaceCodeFixTests_FixAllTests.cs} | 2 +- .../IImplementInterfaceService.cs | 2 + ...s.vb => ImplementInterfaceCodeFixTests.vb} | 2 +- ...ementInterfaceCodeFixTests_FixAllTests.vb} | 2 +- .../VisualBasicAnalyzers.UnitTests.projitems | 4 +- .../ImplementInterfaceCodeRefactoringTests.cs | 20 ++++++++ ...plementInterfaceCodeRefactoringProvider.cs | 47 +++++++++++++++++++ 9 files changed, 77 insertions(+), 8 deletions(-) rename src/Analyzers/CSharp/Tests/ImplementInterface/{ImplementInterfaceTests.cs => ImplementInterfaceCodeFixTests.cs} (99%) rename src/Analyzers/CSharp/Tests/ImplementInterface/{ImplementInterfaceTests_FixAllTests.cs => ImplementInterfaceCodeFixTests_FixAllTests.cs} (99%) rename src/Analyzers/VisualBasic/Tests/ImplementInterface/{ImplementInterfaceTests.vb => ImplementInterfaceCodeFixTests.vb} (99%) rename src/Analyzers/VisualBasic/Tests/ImplementInterface/{ImplementInterfaceTests_FixAllTests.vb => ImplementInterfaceCodeFixTests_FixAllTests.vb} (99%) create mode 100644 src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs create mode 100644 src/Features/Core/Portable/ImplementInterface/ImplementInterfaceCodeRefactoringProvider.cs 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/IImplementInterfaceService.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs index fcd1024695861..9cf22b60876dd 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs @@ -48,4 +48,6 @@ ImmutableArray ImplementInterfaceMember( Task> GetCodeActionsAsync( Document document, SyntaxNode? interfaceType, CancellationToken cancellationToken); + + ImmutableArray GetInterfaceTypes(SyntaxNode typeDeclaration); } 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..fafbab6388764 --- /dev/null +++ b/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs @@ -0,0 +1,20 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.GenerateComparisonOperators; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.ImplementInterface; + +using VerifyCS = CSharpCodeRefactoringVerifier< + GenerateComparisonOperatorsCodeRefactoringProvider>; + +public sealed class ImplementInterfaceCodeRefactoringTests +{ +} diff --git a/src/Features/Core/Portable/ImplementInterface/ImplementInterfaceCodeRefactoringProvider.cs b/src/Features/Core/Portable/ImplementInterface/ImplementInterfaceCodeRefactoringProvider.cs new file mode 100644 index 0000000000000..e95a5500a04d1 --- /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 = nameof(ImplementInterfaceCodeRefactoringProvider)), 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); + } +} From 21db766bf4112f47538c435bd85fe1b76b30e984 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 14 Jul 2025 14:16:02 -0700 Subject: [PATCH 3/6] Add test --- .../CSharpImplementInterfaceService.cs | 12 +++++- ...stractImplementInterfaceCodeFixProvider.cs | 10 ----- ...AbstractImplementInterfaceService.State.cs | 4 +- .../AbstractImplementInterfaceService.cs | 13 ++++++- .../ImplementInterfaceGenerator.cs | 6 +-- .../ImplementInterfaceGenerator_Conflicts.cs | 2 +- ...lementInterfaceGenerator_DisposePattern.cs | 2 +- .../ImplementInterfaceGenerator_Method.cs | 2 +- .../ImplementInterfaceGenerator_Property.cs | 2 +- .../VisualBasicImplementInterfaceService.vb | 11 +++++- .../ImplementInterfaceCodeRefactoringTests.cs | 39 ++++++++++++++++--- 11 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceService.cs b/src/Analyzers/CSharp/CodeFixes/ImplementInterface/CSharpImplementInterfaceService.cs index 714cf151591f2..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; @@ -40,6 +41,15 @@ protected override bool AllowDelegateAndEnumConstraints(ParseOptions options) 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/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs index a165be3c57854..7fffec0d40da3 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceCodeFixProvider.cs @@ -2,23 +2,13 @@ // 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 { 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 57a856052a176..e892d97fc3db9 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs @@ -23,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"; @@ -44,6 +45,16 @@ protected abstract bool TryInitializeState(Document document, SemanticModel mode 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) 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/VisualBasicImplementInterfaceService.vb b/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb index ca90aa58e1fe6..eb0d4a2a879bc 100644 --- a/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb +++ b/src/Analyzers/VisualBasic/CodeFixes/ImplementInterface/VisualBasicImplementInterfaceService.vb @@ -10,6 +10,7 @@ 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 @@ -17,7 +18,7 @@ Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.ImplementInterface Partial Friend NotInheritable Class VisualBasicImplementInterfaceService - Inherits AbstractImplementInterfaceService + Inherits AbstractImplementInterfaceService(Of TypeBlockSyntax) @@ -43,6 +44,14 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.ImplementInterface 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/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs b/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs index fafbab6388764..0a065c3858a28 100644 --- a/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs +++ b/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs @@ -2,19 +2,46 @@ // 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.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; -using Microsoft.CodeAnalysis.GenerateComparisonOperators; +using Microsoft.CodeAnalysis.ImplementInterface; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.ImplementInterface; using VerifyCS = CSharpCodeRefactoringVerifier< - GenerateComparisonOperatorsCodeRefactoringProvider>; + 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(); + } + } + """); } From 2348ef1d181cfd5776ead201143f6d899ef32714 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 14 Jul 2025 14:29:39 -0700 Subject: [PATCH 4/6] Add test --- .../ImplementInterfaceCodeRefactoringTests.cs | 14 +++++ .../VisualBasicCodeRefactoringVerifier`1.cs | 7 +++ .../ImplementInterfaceCodeRefactoringTests.vb | 55 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 src/Features/VisualBasicTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.vb diff --git a/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs b/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs index 0a065c3858a28..820dbc26a4f4a 100644 --- a/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs +++ b/src/Features/CSharpTest/ImplementInterface/ImplementInterfaceCodeRefactoringTests.cs @@ -44,4 +44,18 @@ public void Goo() } } """); + + [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/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 From 2d825d8ba1a234f66640605d3ef2614f2a52f6f9 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 14 Jul 2025 15:34:48 -0700 Subject: [PATCH 5/6] Add predefined name --- .../CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs | 1 + .../ImplementInterfaceCodeRefactoringProvider.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 index e95a5500a04d1..c75c4a473a419 100644 --- a/src/Features/Core/Portable/ImplementInterface/ImplementInterfaceCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ImplementInterface/ImplementInterfaceCodeRefactoringProvider.cs @@ -14,7 +14,7 @@ namespace Microsoft.CodeAnalysis.ImplementInterface; [ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, - Name = nameof(ImplementInterfaceCodeRefactoringProvider)), Shared] + Name = PredefinedCodeRefactoringProviderNames.ImplementInterface), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class ImplementInterfaceCodeRefactoringProvider() : CodeRefactoringProvider From 56cc8e942a9bbd75950aba986d992380852dd0c5 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 14 Jul 2025 17:20:48 -0700 Subject: [PATCH 6/6] Remove members from interface --- .../AbstractImplementInterfaceService.cs | 4 ++-- .../ImplementInterface/IImplementInterfaceService.cs | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs index e892d97fc3db9..0d85020a1be91 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/AbstractImplementInterfaceService.cs @@ -76,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; @@ -105,7 +105,7 @@ protected SyntaxTriviaList CreateCommentTrivia( return [.. trivia]; } - public async Task ImplementInterfaceAsync( + private async Task ImplementInterfaceAsync( Document document, ImplementInterfaceInfo info, ImplementTypeOptions options, diff --git a/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs b/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs index 9cf22b60876dd..32df9beb85355 100644 --- a/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs +++ b/src/Analyzers/Core/CodeFixes/ImplementInterface/IImplementInterfaceService.cs @@ -24,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