File: Copilot\CSharpImplementNotImplementedExceptionFixProviderTests.cs
Web Access
Project: src\src\Features\CSharpTest\Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.Features.UnitTests)
// 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.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Copilot;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Xunit;
 
namespace Microsoft.CodeAnalysis.CSharp.Copilot.UnitTests;
 
using VerifyCS = CSharpCodeFixVerifier<
    CSharpImplementNotImplementedExceptionDiagnosticAnalyzer,
    CSharpImplementNotImplementedExceptionFixProvider>;
 
[UseExportProvider]
[Trait(Traits.Feature, Traits.Features.CopilotImplementNotImplementedException)]
public sealed partial class CSharpImplementNotImplementedExceptionFixProviderTests
{
    [Fact]
    public async Task FixAll_ParseSuccessfully()
    {
        await new CustomCompositionCSharpTest
        {
            TestCode = """
            using System;
            using System.Threading.Tasks;
 
            public class MathService : IMathService
            {
                public int Add(int a, int b)
                {
                    {|IDE3000:throw new NotImplementedException("Add method not implemented");|}
                }
            
                public int Subtract(int a, int b) => {|IDE3000:throw new NotImplementedException("Subtract method not implemented")|};
            
                public int Multiply(int a, int b) {
                    {|IDE3000:throw new NotImplementedException("Multiply method not implemented");|}
                }
            
                public double Divide(int a, int b)
                {
                    {|IDE3000:throw new NotImplementedException("Divide method not implemented");|}
                }
            
                public double CalculateSquareRoot(double number) => {|IDE3000:throw new NotImplementedException("CalculateSquareRoot method not implemented")|};
            
                public int Factorial(int number)
                {
                    {|IDE3000:throw new NotImplementedException("Factorial method not implemented");|}
                }
            
                public int ConstantValue => {|IDE3000:throw new NotImplementedException("Property not implemented")|};
            
                public MathService()
                {
                    {|IDE3000:throw new NotImplementedException("Constructor not implemented");|}
                }
            
                ~MathService()
                {
                    {|IDE3000:throw new NotImplementedException("Destructor not implemented");|}
                }
            
                public event EventHandler MyEvent
                {
                    add { {|IDE3000:throw new NotImplementedException("Event add not implemented");|} }
                    remove { {|IDE3000:throw new NotImplementedException("Event remove not implemented");|} }
                }
            
                public static MathService operator +(MathService a, MathService b)
                {
                    {|IDE3000:throw new NotImplementedException("Operator not implemented");|}
                }
            }
 
            public interface IMathService
            {
                int Add(int a, int b);
                int Subtract(int a, int b);
                int Multiply(int a, int b);
                double Divide(int a, int b);
                double CalculateSquareRoot(double number);
                int Factorial(int number);
                int ConstantValue { get; }
                event EventHandler MyEvent;
            }
            """,
            FixedCode = """
            using System;
            using System.Threading.Tasks;
 
            public class MathService : IMathService
            {
                public int Add(int a, int b)
                {
                    return a + b;
                }
            
                public int Subtract(int a, int b) => a - b;
            
                public int Multiply(int a, int b)
                {
                    return a * b;
                }
            
                public double Divide(int a, int b)
                {
                    if (b == 0) throw new DivideByZeroException("Division by zero is not allowed");
                    return (double)a / b;
                }
            
                public double CalculateSquareRoot(double number) => Math.Sqrt(number);
            
                public int Factorial(int number)
                {
                    if (number < 0) throw new ArgumentException("Number must be non-negative", nameof(number));
                    return number == 0 ? 1 : number * Factorial(number - 1);
                }
            
                public int ConstantValue => 42;
            
                public MathService()
                {
                    // Constructor implementation
                }
 
                ~MathService()
                {
                    // Destructor implementation
                }
            
                public event EventHandler MyEvent
                {
                    add { /* Event add implementation */ }
                    remove { /* Event remove implementation */ }
                }
            
                public static MathService operator +(MathService a, MathService b)
                {
                    return new MathService(); // Operator implementation
                }
            }
            
            public interface IMathService
            {
                int Add(int a, int b);
                int Subtract(int a, int b);
                int Multiply(int a, int b);
                double Divide(int a, int b);
                double CalculateSquareRoot(double number);
                int Factorial(int number);
                int ConstantValue { get; }
                event EventHandler MyEvent;
            }
            """,
            LanguageVersion = LanguageVersion.CSharp11,
            ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
        }
        .WithMockCopilotService(copilotService =>
        {
            copilotService.SetupFixAll = (Document document, ImmutableDictionary<MemberDeclarationSyntax, ImmutableArray<ReferencedSymbol>> memberReferences, CancellationToken cancellationToken) =>
            {
                // Create a map of method/property implementations
                var implementationMap = new Dictionary<string, string>
                {
                    ["Add"] = "public int Add(int a, int b)\n{\n    return a + b;\n}\n",
                    ["Subtract"] = "public int Subtract(int a, int b) => a - b;\n",
                    ["Multiply"] = "public int Multiply(int a, int b)\n{\n    return a * b;\n}\n",
                    ["Divide"] = "public double Divide(int a, int b)\n{\n    if (b == 0) throw new DivideByZeroException(\"Division by zero is not allowed\");\n    return (double)a / b;\n}\n",
                    ["CalculateSquareRoot"] = "public double CalculateSquareRoot(double number) => Math.Sqrt(number);\n",
                    ["Factorial"] = "public int Factorial(int number)\n{\n    if (number < 0) throw new ArgumentException(\"Number must be non-negative\", nameof(number));\n    return number == 0 ? 1 : number * Factorial(number - 1);\n}\n",
                    ["ConstantValue"] = "public int ConstantValue => 42;\n",
                    ["MathService"] = "public MathService()\n{\n    // Constructor implementation\n}\n",
                    ["~MathService"] = "~MathService()\n{\n    // Destructor implementation\n}\n",
                    ["MyEvent"] = "public event EventHandler MyEvent\n{\n    add { /* Event add implementation */ }\n    remove { /* Event remove implementation */ }\n}\n",
                    ["operator +"] = "public static MathService operator +(MathService a, MathService b)\n{\n    return new MathService(); // Operator implementation\n}\n"
                };
 
                // Process each member reference and create implementation details
                var resultsBuilder = ImmutableDictionary.CreateBuilder<MemberDeclarationSyntax, ImplementationDetails>();
                foreach (var memberReference in memberReferences)
                {
                    var memberNode = memberReference.Key;
 
                    // Get the identifier based on node type
                    var identifier = memberNode switch
                    {
                        MethodDeclarationSyntax method => method.Identifier.Text,
                        PropertyDeclarationSyntax property => property.Identifier.Text,
                        ConstructorDeclarationSyntax constructor => constructor.Identifier.Text,
                        DestructorDeclarationSyntax destructor => destructor.TildeToken.Text + destructor.Identifier.Text,
                        EventDeclarationSyntax @event => @event.Identifier.Text,
                        OperatorDeclarationSyntax @operator => "operator " + @operator.OperatorToken.Text,
                        _ => string.Empty
                    };
 
                    // Look up implementation in our map
                    Assumes.True(implementationMap.TryGetValue(identifier, out var implementation));
                    resultsBuilder.Add(
                        memberNode,
                        new ImplementationDetails
                        {
                            ReplacementNode = SyntaxFactory.ParseMemberDeclaration(implementation),
                        });
                }
 
                return resultsBuilder.ToImmutable();
            };
        })
        .RunAsync();
    }
 
    [Theory]
    [InlineData("Failed to receive implementation from Copilot service")]
    [InlineData("Generated implementation doesn't match the original method signature.")]
    [InlineData("The generated implementation isn't a valid method or property.")]
    public async Task NullReplacementNode_MethodHasCommentsInVariousForms_PrintsMessageAsComment(string copilotErrorMessage)
    {
        await new CustomCompositionCSharpTest
        {
            CodeFixTestBehaviors = CodeFixTestBehaviors.FixOne,
            TestCode = """
            using System;
            using System.Threading.Tasks;
 
            public class DataService : IDataService
            {
                public void AddData(string data)
                {
                    {|IDE3000:throw new NotImplementedException("AddData method not implemented");|}
                }
 
                public string GetData(int id) => {|IDE3000:throw new NotImplementedException()|};
 
                /* Updates the data for a given ID */
                public void UpdateData(int id, string data)
                {
                    if (id <= 0) throw new ArgumentException("ID must be greater than zero", nameof(id));
                    {|IDE3000:throw new NotImplementedException("UpdateData method not implemented");|}
                }
 
                // Deletes data by ID
                public void DeleteData(int id)
                {
                    if (id <= 0) throw new ArgumentException("ID must be greater than zero", nameof(id));
                    {|IDE3000:throw new NotImplementedException();|}
                }
 
                /// <summary>
                /// Saves changes asynchronously
                /// </summary>
                /// <returns>A task representing the save operation</returns>
                public Task SaveChangesAsync()
                {
                    {|IDE3000:throw new NotImplementedException("SaveChangesAsync method not implemented");|}
                }
 
                public int DataCount => {|IDE3000:throw new NotImplementedException("Property not implemented")|};
            }
 
            public interface IDataService
            {
                void AddData(string data);
                string GetData(int id);
                void UpdateData(int id, string data);
                void DeleteData(int id);
                Task SaveChangesAsync();
                int DataCount { get; }
            }
            """,
            FixedCode = $$"""
            using System;
            using System.Threading.Tasks;
 
            public class DataService : IDataService
            {
                /* {{copilotErrorMessage}} */
                public void AddData(string data)
                {
                    {|IDE3000:throw new NotImplementedException("AddData method not implemented");|}
                }
            
                public string GetData(int id) => {|IDE3000:throw new NotImplementedException()|};
            
                /* Updates the data for a given ID */
                public void UpdateData(int id, string data)
                {
                    if (id <= 0) throw new ArgumentException("ID must be greater than zero", nameof(id));
                    {|IDE3000:throw new NotImplementedException("UpdateData method not implemented");|}
                }
            
                // Deletes data by ID
                public void DeleteData(int id)
                {
                    if (id <= 0) throw new ArgumentException("ID must be greater than zero", nameof(id));
                    {|IDE3000:throw new NotImplementedException();|}
                }
            
                /// <summary>
                /// Saves changes asynchronously
                /// </summary>
                /// <returns>A task representing the save operation</returns>
                public Task SaveChangesAsync()
                {
                    {|IDE3000:throw new NotImplementedException("SaveChangesAsync method not implemented");|}
                }
            
                public int DataCount => {|IDE3000:throw new NotImplementedException("Property not implemented")|};
            }
 
            public interface IDataService
            {
                void AddData(string data);
                string GetData(int id);
                void UpdateData(int id, string data);
                void DeleteData(int id);
                Task SaveChangesAsync();
                int DataCount { get; }
            }
            """,
            BatchFixedCode = $$"""
            using System;
            using System.Threading.Tasks;
 
            public class DataService : IDataService
            {
                /* {{copilotErrorMessage}} */
                public void AddData(string data)
                {
                    {|IDE3000:throw new NotImplementedException("AddData method not implemented");|}
                }
            
                /* {{copilotErrorMessage}} */
                public string GetData(int id) => {|IDE3000:throw new NotImplementedException()|};
            
                /* Updates the data for a given ID */
                /* {{copilotErrorMessage}} */
                public void UpdateData(int id, string data)
                {
                    if (id <= 0) throw new ArgumentException("ID must be greater than zero", nameof(id));
                    {|IDE3000:throw new NotImplementedException("UpdateData method not implemented");|}
                }
            
                // Deletes data by ID
                /* {{copilotErrorMessage}} */
                public void DeleteData(int id)
                {
                    if (id <= 0) throw new ArgumentException("ID must be greater than zero", nameof(id));
                    {|IDE3000:throw new NotImplementedException();|}
                }
            
                /* {{copilotErrorMessage}} */
                /// <summary>
                /// Saves changes asynchronously
                /// </summary>
                /// <returns>A task representing the save operation</returns>
                public Task SaveChangesAsync()
                {
                    {|IDE3000:throw new NotImplementedException("SaveChangesAsync method not implemented");|}
                }
            
                /* {{copilotErrorMessage}} */
                public int DataCount => {|IDE3000:throw new NotImplementedException("Property not implemented")|};
            }
 
            public interface IDataService
            {
                void AddData(string data);
                string GetData(int id);
                void UpdateData(int id, string data);
                void DeleteData(int id);
                Task SaveChangesAsync();
                int DataCount { get; }
            }
            """,
            FixedState =
            {
                MarkupHandling = MarkupMode.Allow,
            },
            BatchFixedState =
            {
                MarkupHandling = MarkupMode.Allow,
            },
            LanguageVersion = LanguageVersion.CSharp11,
            ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
        }
        .WithMockCopilotService(copilotService =>
        {
            copilotService.PrepareUsingSingleFakeResult = new()
            {
                ReplacementNode = null,
                Message = copilotErrorMessage,
            };
        })
        .RunAsync();
    }
 
    [Fact]
    public async Task HandleInvalidCode_SuggestsAsComment()
    {
        await new CustomCompositionCSharpTest
        {
            CodeFixTestBehaviors = CodeFixTestBehaviors.FixOne,
            TestCode = """
            using System;
 
            class C
            {
                void M()
                {
                    {|IDE3000:throw new NotImplementedException();|}
                }
            }
            """,
            FixedCode = """
            using System;
 
            class C
            {
                /* The generated implementation isn't a valid method or property:
                using System;
                class C
                {
                    void M()
                    {
                        throw new NotImplementedException();
                    }
                } */
                void M()
                {
                    {|IDE3000:throw new NotImplementedException();|}
                }
            }
            """,
            FixedState =
            {
                MarkupHandling = MarkupMode.Allow,
            },
            LanguageVersion = LanguageVersion.CSharp11,
            ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
        }
        .WithMockCopilotService(copilotService =>
        {
            var replacement = """
            using System;
            class C
            {
                void M()
                {
                    throw new NotImplementedException();
                }
            }
            """;
            copilotService.PrepareUsingSingleFakeResult = new()
            {
                ReplacementNode = SyntaxFactory.ParseCompilationUnit(replacement),
                Message = $"The generated implementation isn't a valid method or property:{Environment.NewLine}{replacement}",
            };
        })
        .RunAsync();
    }
 
    [Fact]
    public async Task ReplacementNode_Null_NotifiesWithComment()
    {
        await TestHandlesInvalidReplacementNode(
            new()
            {
                ReplacementNode = null,
                Message = "Custom Error Message",
            });
    }
 
    [Theory]
    [InlineData("class MyClass { }", typeof(ClassDeclarationSyntax))]
    [InlineData("struct MyStruct { }", typeof(StructDeclarationSyntax))]
    [InlineData("interface IMyInterface { }", typeof(InterfaceDeclarationSyntax))]
    [InlineData("enum MyEnum { Value1, Value2 }", typeof(EnumDeclarationSyntax))]
    [InlineData("delegate void MyDelegate();", typeof(DelegateDeclarationSyntax))]
    [InlineData("int myField;", typeof(FieldDeclarationSyntax))]
    [InlineData("event EventHandler MyEvent;", typeof(EventFieldDeclarationSyntax))]
    [InlineData("record MyRecord { }", typeof(RecordDeclarationSyntax))]
    public async Task TestInvalidNodeReplacement(string syntax, Type type)
    {
        await TestHandlesInvalidReplacementNode(
            new()
            {
                ReplacementNode = SyntaxFactory.ParseMemberDeclaration(syntax),
                Message = $"Copilot response is of type {type}, but expected method or property",
            })
            .ConfigureAwait(false);
    }
 
    [Theory]
    [InlineData(" ")]
    [InlineData("")]
    public async Task TestHandlesEmptyReplacementNode(string emptyReplacement)
    {
        await TestHandlesInvalidReplacementNode(
            new()
            {
                ReplacementNode = SyntaxFactory.ParseMemberDeclaration(emptyReplacement),
                Message = "Custom Error Message",
            })
            .ConfigureAwait(false);
    }
 
    private static async Task TestHandlesInvalidReplacementNode(ImplementationDetails implementationDetails)
    {
        Assumes.False(string.IsNullOrWhiteSpace(implementationDetails.Message));
        await new CustomCompositionCSharpTest
        {
            CodeFixTestBehaviors = CodeFixTestBehaviors.FixOne,
            TestCode = """
            using System;
 
            class C
            {
                void M()
                {
                    {|IDE3000:throw new NotImplementedException();|}
                }
            }
            """,
            FixedCode = $$"""
            using System;
 
            class C
            {
                /* {{implementationDetails.Message}} */
                void M()
                {
                    {|IDE3000:throw new NotImplementedException();|}
                }
            }
            """,
            FixedState =
            {
                MarkupHandling = MarkupMode.Allow,
            },
            LanguageVersion = LanguageVersion.CSharp11,
            ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
        }
        .WithMockCopilotService(copilotService =>
        {
            copilotService.PrepareUsingSingleFakeResult = implementationDetails;
        })
        .RunAsync();
    }
 
    private class CustomCompositionCSharpTest : VerifyCS.Test
    {
        private TestComposition? _testComposition;
        private TestWorkspace? _testWorkspace;
        private Action<TestCopilotCodeAnalysisService>? _copilotServiceSetupAction;
 
        protected override Task<Workspace> CreateWorkspaceImplAsync()
        {
            _testComposition = FeaturesTestCompositions.Features
                .AddParts([typeof(TestCopilotOptionsService), typeof(TestCopilotCodeAnalysisService)]);
            _testWorkspace = new TestWorkspace(_testComposition);
 
            // Trigger the action if it's set
            _copilotServiceSetupAction?.Invoke(GetCopilotService(_testWorkspace));
            return Task.FromResult<Workspace>(_testWorkspace);
        }
 
        public CustomCompositionCSharpTest WithMockCopilotService(Action<TestCopilotCodeAnalysisService> setup)
        {
            _copilotServiceSetupAction = setup;
 
            // If _testWorkspace is already set, trigger the action immediately
            if (_testWorkspace != null)
            {
                setup(GetCopilotService(_testWorkspace));
            }
 
            return this;
        }
 
        private static TestCopilotCodeAnalysisService GetCopilotService(TestWorkspace testWorkspace)
        {
            var copilotService = testWorkspace.Services.GetLanguageServices(LanguageNames.CSharp)
                .GetRequiredService<ICopilotCodeAnalysisService>() as TestCopilotCodeAnalysisService;
            Assert.NotNull(copilotService);
            return copilotService;
        }
    }
 
    [ExportLanguageService(typeof(ICopilotOptionsService), LanguageNames.CSharp), Shared, PartNotDiscoverable]
    private sealed class TestCopilotOptionsService : ICopilotOptionsService
    {
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public TestCopilotOptionsService() { }
 
        public Task<bool> IsRefineOptionEnabledAsync()
            => Task.FromResult(true);
 
        public Task<bool> IsCodeAnalysisOptionEnabledAsync()
            => Task.FromResult(true);
 
        public Task<bool> IsOnTheFlyDocsOptionEnabledAsync()
            => Task.FromResult(true);
 
        public Task<bool> IsGenerateDocumentationCommentOptionEnabledAsync()
            => Task.FromResult(true);
 
        public Task<bool> IsImplementNotImplementedExceptionEnabledAsync()
            => Task.FromResult(true);
    }
 
    [ExportLanguageService(typeof(ICopilotCodeAnalysisService), LanguageNames.CSharp), Shared, PartNotDiscoverable]
    private sealed class TestCopilotCodeAnalysisService : ICopilotCodeAnalysisService
    {
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public TestCopilotCodeAnalysisService()
        {
        }
 
        public Func<Document, ImmutableDictionary<MemberDeclarationSyntax, ImmutableArray<ReferencedSymbol>>, CancellationToken, ImmutableDictionary<MemberDeclarationSyntax, ImplementationDetails>>? SetupFixAll { get; internal set; }
 
        public ImplementationDetails? PrepareUsingSingleFakeResult { get; internal set; }
 
        public Task AnalyzeDocumentAsync(Document document, TextSpan? span, string promptTitle, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<ImmutableArray<string>> GetAvailablePromptTitlesAsync(Document document, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<ImmutableArray<Diagnostic>> GetCachedDocumentDiagnosticsAsync(Document document, TextSpan? span, ImmutableArray<string> promptTitles, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsAsync(string symbolSignature, ImmutableArray<string> declarationCode, string language, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
            => Task.FromResult(true);
 
        public Task<bool> IsFileExcludedAsync(string filePath, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task StartRefinementSessionAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        Task<(Dictionary<string, string>? responseDictionary, bool isQuotaExceeded)> ICopilotCodeAnalysisService.GetDocumentationCommentAsync(DocumentationCommentProposal proposal, CancellationToken cancellationToken)
            => throw new NotImplementedException();
 
        public Task<ImmutableDictionary<MemberDeclarationSyntax, ImplementationDetails>> ImplementNotImplementedExceptionsAsync(
            Document document,
            ImmutableDictionary<MemberDeclarationSyntax, ImmutableArray<ReferencedSymbol>> methodOrProperties,
            CancellationToken cancellationToken)
        {
            if (SetupFixAll != null)
            {
                return Task.FromResult(SetupFixAll.Invoke(document, methodOrProperties, cancellationToken));
            }
 
            if (PrepareUsingSingleFakeResult != null)
            {
                return Task.FromResult(CreateSingleNodeResult(methodOrProperties, PrepareUsingSingleFakeResult));
            }
 
            return Task.FromResult(ImmutableDictionary<MemberDeclarationSyntax, ImplementationDetails>.Empty);
        }
 
        private static ImmutableDictionary<MemberDeclarationSyntax, ImplementationDetails> CreateSingleNodeResult(
            ImmutableDictionary<MemberDeclarationSyntax, ImmutableArray<ReferencedSymbol>> methodOrProperties,
            ImplementationDetails implementationDetails)
        {
            var resultsBuilder = ImmutableDictionary.CreateBuilder<MemberDeclarationSyntax, ImplementationDetails>();
            foreach (var methodOrProperty in methodOrProperties)
            {
                resultsBuilder.Add(methodOrProperty.Key, implementationDetails);
            }
 
            return resultsBuilder.ToImmutable();
        }
 
        public Task<bool> IsImplementNotImplementedExceptionsAvailableAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(true);
        }
    }
}