File: CodeFixes\CodeFixServiceTests.cs
Web Access
Project: ..\..\..\src\EditorFeatures\Test\Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.EditorFeatures.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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.ErrorLogger;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.UnitTests.CodeFixes
{
    [UseExportProvider]
    public class CodeFixServiceTests
    {
        private static readonly TestComposition s_compositionWithMockDiagnosticUpdateSourceRegistrationService = EditorTestCompositions.EditorFeatures
            .AddExcludedPartTypes(typeof(IDiagnosticUpdateSourceRegistrationService))
            .AddParts(typeof(MockDiagnosticUpdateSourceRegistrationService));
 
        [Fact]
        public async Task TestGetFirstDiagnosticWithFixAsync()
        {
            var fixers = CreateFixers();
            var code = @"
    a
";
            using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithMockDiagnosticUpdateSourceRegistrationService, openDocuments: true);
 
            Assert.IsType<MockDiagnosticUpdateSourceRegistrationService>(workspace.GetService<IDiagnosticUpdateSourceRegistrationService>());
            var diagnosticService = Assert.IsType<DiagnosticAnalyzerService>(workspace.GetService<IDiagnosticAnalyzerService>());
 
            var analyzerReference = new TestAnalyzerReferenceByLanguage(DiagnosticExtensions.GetCompilerDiagnosticAnalyzersMap());
            workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(new[] { analyzerReference }));
 
            var logger = SpecializedCollections.SingletonEnumerable(new Lazy<IErrorLoggerService>(() => workspace.Services.GetRequiredService<IErrorLoggerService>()));
            var fixService = new CodeFixService(
                diagnosticService, logger, fixers, SpecializedCollections.EmptyEnumerable<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>>());
 
            var incrementalAnalyzer = (IIncrementalAnalyzerProvider)diagnosticService;
 
            // register diagnostic engine to solution crawler
            var analyzer = incrementalAnalyzer.CreateIncrementalAnalyzer(workspace);
 
            var reference = new MockAnalyzerReference();
            var project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(reference);
            var document = project.Documents.Single();
            var unused = await fixService.GetMostSevereFixAsync(
                document, TextSpan.FromBounds(0, 0), CodeActionRequestPriority.None, CodeActionOptions.DefaultProvider, isBlocking: false, CancellationToken.None);
 
            var fixer1 = (MockFixer)fixers.Single().Value;
            var fixer2 = (MockFixer)reference.Fixers.Single();
 
            // check to make sure both of them are called.
            Assert.True(fixer1.Called);
            Assert.True(fixer2.Called);
        }
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/41116")]
        public async Task TestGetFixesAsyncWithDuplicateDiagnostics()
        {
            var codeFix = new MockFixer();
 
            // Add duplicate analyzers to get duplicate diagnostics.
            var analyzerReference = new MockAnalyzerReference(
                    codeFix,
                    ImmutableArray.Create<DiagnosticAnalyzer>(
                        new MockAnalyzerReference.MockDiagnosticAnalyzer(),
                        new MockAnalyzerReference.MockDiagnosticAnalyzer()));
 
            var tuple = ServiceSetup(codeFix);
            using var workspace = tuple.workspace;
            GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document, out var extensionManager, analyzerReference);
 
            // Verify that we do not crash when computing fixes.
            _ = await tuple.codeFixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0), CodeActionOptions.DefaultProvider, isBlocking: false, CancellationToken.None);
 
            // Verify that code fix is invoked with both the diagnostics in the context,
            // i.e. duplicate diagnostics are not silently discarded by the CodeFixService.
            Assert.Equal(2, codeFix.ContextDiagnosticsCount);
        }
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/45779")]
        public async Task TestGetFixesAsyncHasNoDuplicateConfigurationActions()
        {
            var codeFix = new MockFixer();
 
            // Add analyzers with duplicate ID and/or category to get duplicate diagnostics.
            var analyzerReference = new MockAnalyzerReference(
                    codeFix,
                    ImmutableArray.Create<DiagnosticAnalyzer>(
                        new MockAnalyzerReference.MockDiagnosticAnalyzer("ID1", "Category1"),
                        new MockAnalyzerReference.MockDiagnosticAnalyzer("ID1", "Category1"),
                        new MockAnalyzerReference.MockDiagnosticAnalyzer("ID1", "Category2"),
                        new MockAnalyzerReference.MockDiagnosticAnalyzer("ID2", "Category2")));
 
            var tuple = ServiceSetup(codeFix, includeConfigurationFixProviders: true);
            using var workspace = tuple.workspace;
            GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document, out var extensionManager, analyzerReference);
 
            // Verify registered configuration code actions do not have duplicates.
            var fixCollections = await tuple.codeFixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0), CodeActionOptions.DefaultProvider, isBlocking: false, CancellationToken.None);
            var codeActions = fixCollections.SelectMany(c => c.Fixes.Select(f => f.Action)).ToImmutableArray();
            Assert.Equal(7, codeActions.Length);
            var uniqueTitles = new HashSet<string>();
            foreach (var codeAction in codeActions)
            {
                Assert.True(codeAction is AbstractConfigurationActionWithNestedActions);
                Assert.True(uniqueTitles.Add(codeAction.Title));
            }
        }
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/56843")]
        public async Task TestGetFixesAsyncForFixableAndNonFixableAnalyzersAsync()
        {
            var codeFix = new MockFixer();
            var analyzerWithFix = new MockAnalyzerReference.MockDiagnosticAnalyzer();
            Assert.Equal(codeFix.FixableDiagnosticIds.Single(), analyzerWithFix.SupportedDiagnostics.Single().Id);
 
            var analyzerWithoutFix = new MockAnalyzerReference.MockDiagnosticAnalyzer("AnalyzerWithoutFixId", "Category");
            var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(analyzerWithFix, analyzerWithoutFix);
            var analyzerReference = new MockAnalyzerReference(codeFix, analyzers);
 
            // Verify no callbacks received at initialization.
            Assert.False(analyzerWithFix.ReceivedCallback);
            Assert.False(analyzerWithoutFix.ReceivedCallback);
 
            var tuple = ServiceSetup(codeFix, includeConfigurationFixProviders: true);
            using var workspace = tuple.workspace;
            GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document, out var extensionManager, analyzerReference);
 
            // Verify only analyzerWithFix is executed when GetFixesAsync is invoked with 'CodeActionRequestPriority.Normal'.
            _ = await tuple.codeFixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0),
                priority: CodeActionRequestPriority.Normal, CodeActionOptions.DefaultProvider, isBlocking: false,
                addOperationScope: _ => null, cancellationToken: CancellationToken.None);
            Assert.True(analyzerWithFix.ReceivedCallback);
            Assert.False(analyzerWithoutFix.ReceivedCallback);
 
            // Verify both analyzerWithFix and analyzerWithoutFix are executed when GetFixesAsync is invoked with 'CodeActionRequestPriority.Lowest'.
            _ = await tuple.codeFixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0),
                priority: CodeActionRequestPriority.Lowest, CodeActionOptions.DefaultProvider, isBlocking: false,
                addOperationScope: _ => null, cancellationToken: CancellationToken.None);
            Assert.True(analyzerWithFix.ReceivedCallback);
            Assert.True(analyzerWithoutFix.ReceivedCallback);
        }
 
        [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1450689")]
        public async Task TestGetFixesAsyncForDocumentDiagnosticAnalyzerAsync()
        {
            // TS has special DocumentDiagnosticAnalyzer that report 0 SupportedDiagnostics.
            // We need to ensure that we don't skip these document analyzers
            // when computing the diagnostics/code fixes for "Normal" priority bucket, which
            // normally only execute those analyzers which report at least one fixable supported diagnostic.
            var documentDiagnosticAnalyzer = new MockAnalyzerReference.MockDocumentDiagnosticAnalyzer(reportedDiagnosticIds: ImmutableArray<string>.Empty);
            Assert.Empty(documentDiagnosticAnalyzer.SupportedDiagnostics);
 
            var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(documentDiagnosticAnalyzer);
            var codeFix = new MockFixer();
            var analyzerReference = new MockAnalyzerReference(codeFix, analyzers);
 
            // Verify no callbacks received at initialization.
            Assert.False(documentDiagnosticAnalyzer.ReceivedCallback);
 
            var tuple = ServiceSetup(codeFix, includeConfigurationFixProviders: false);
            using var workspace = tuple.workspace;
            GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document, out var extensionManager, analyzerReference);
 
            // Verify both analyzers are executed when GetFixesAsync is invoked with 'CodeActionRequestPriority.Normal'.
            _ = await tuple.codeFixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0),
                priority: CodeActionRequestPriority.Normal, CodeActionOptions.DefaultProvider, isBlocking: false,
                addOperationScope: _ => null, cancellationToken: CancellationToken.None);
            Assert.True(documentDiagnosticAnalyzer.ReceivedCallback);
        }
 
        [Fact]
        public async Task TestGetCodeFixWithExceptionInRegisterMethod_Diagnostic()
        {
            await GetFirstDiagnosticWithFixWithExceptionValidationAsync(new ErrorCases.ExceptionInRegisterMethod());
        }
 
        [Fact]
        public async Task TestGetCodeFixWithExceptionInRegisterMethod_Fixes()
        {
            await GetAddedFixesWithExceptionValidationAsync(new ErrorCases.ExceptionInRegisterMethod());
        }
 
        [Fact]
        public async Task TestGetCodeFixWithExceptionInRegisterMethodAsync_Diagnostic()
        {
            await GetFirstDiagnosticWithFixWithExceptionValidationAsync(new ErrorCases.ExceptionInRegisterMethodAsync());
        }
 
        [Fact]
        public async Task TestGetCodeFixWithExceptionInRegisterMethodAsync_Fixes()
        {
            await GetAddedFixesWithExceptionValidationAsync(new ErrorCases.ExceptionInRegisterMethodAsync());
        }
 
        [Fact]
        public async Task TestGetCodeFixWithExceptionInFixableDiagnosticIds_Diagnostic()
        {
            await GetFirstDiagnosticWithFixWithExceptionValidationAsync(new ErrorCases.ExceptionInFixableDiagnosticIds());
        }
 
        [Fact]
        public async Task TestGetCodeFixWithExceptionInFixableDiagnosticIds_Fixes()
        {
            await GetAddedFixesWithExceptionValidationAsync(new ErrorCases.ExceptionInFixableDiagnosticIds());
        }
 
        [Fact(Skip = "https://github.com/dotnet/roslyn/issues/21533")]
        public async Task TestGetCodeFixWithExceptionInFixableDiagnosticIds_Diagnostic2()
        {
            await GetFirstDiagnosticWithFixWithExceptionValidationAsync(new ErrorCases.ExceptionInFixableDiagnosticIds2());
        }
 
        [Fact(Skip = "https://github.com/dotnet/roslyn/issues/21533")]
        public async Task TestGetCodeFixWithExceptionInFixableDiagnosticIds_Fixes2()
        {
            await GetAddedFixesWithExceptionValidationAsync(new ErrorCases.ExceptionInFixableDiagnosticIds2());
        }
 
        [Fact]
        public async Task TestGetCodeFixWithExceptionInGetFixAllProvider()
            => await GetAddedFixesWithExceptionValidationAsync(new ErrorCases.ExceptionInGetFixAllProvider());
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/45851")]
        public async Task TestGetCodeFixWithExceptionOnCodeFixProviderCreation()
            => await GetAddedFixesAsync(
                new MockFixer(),
                new MockAnalyzerReference.MockDiagnosticAnalyzer(),
                throwExceptionInFixerCreation: true);
 
        private static Task<ImmutableArray<CodeFixCollection>> GetAddedFixesWithExceptionValidationAsync(CodeFixProvider codefix)
            => GetAddedFixesAsync(codefix, diagnosticAnalyzer: new MockAnalyzerReference.MockDiagnosticAnalyzer(), exception: true);
 
        private static async Task<ImmutableArray<CodeFixCollection>> GetAddedFixesAsync(CodeFixProvider codefix, DiagnosticAnalyzer diagnosticAnalyzer, bool exception = false, bool throwExceptionInFixerCreation = false)
        {
            var tuple = ServiceSetup(codefix, throwExceptionInFixerCreation: throwExceptionInFixerCreation);
 
            using var workspace = tuple.workspace;
 
            var errorReportingService = (TestErrorReportingService)workspace.Services.GetRequiredService<IErrorReportingService>();
 
            var errorReported = false;
            errorReportingService.OnError = message => errorReported = true;
 
            GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document, out var extensionManager);
            var incrementalAnalyzer = (IIncrementalAnalyzerProvider)tuple.analyzerService;
            var analyzer = incrementalAnalyzer.CreateIncrementalAnalyzer(workspace);
            var reference = new MockAnalyzerReference(codefix, ImmutableArray.Create(diagnosticAnalyzer));
            var project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(reference);
            document = project.Documents.Single();
            var fixes = await tuple.codeFixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0), CodeActionOptions.DefaultProvider, isBlocking: false, CancellationToken.None);
 
            if (exception)
            {
                Assert.True(extensionManager.IsDisabled(codefix));
                Assert.False(extensionManager.IsIgnored(codefix));
            }
 
            Assert.Equal(exception || throwExceptionInFixerCreation, errorReported);
 
            return fixes;
        }
 
        private static async Task GetFirstDiagnosticWithFixWithExceptionValidationAsync(CodeFixProvider codefix)
        {
            var tuple = ServiceSetup(codefix);
            using var workspace = tuple.workspace;
 
            var errorReportingService = (TestErrorReportingService)workspace.Services.GetRequiredService<IErrorReportingService>();
 
            var errorReported = false;
            errorReportingService.OnError = message => errorReported = true;
 
            GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document, out var extensionManager);
            var unused = await tuple.codeFixService.GetMostSevereFixAsync(
                document, TextSpan.FromBounds(0, 0), CodeActionRequestPriority.None, CodeActionOptions.DefaultProvider, isBlocking: false, CancellationToken.None);
            Assert.True(extensionManager.IsDisabled(codefix));
            Assert.False(extensionManager.IsIgnored(codefix));
            Assert.True(errorReported);
        }
 
        private static (TestWorkspace workspace, DiagnosticAnalyzerService analyzerService, CodeFixService codeFixService, IErrorLoggerService errorLogger) ServiceSetup(
            CodeFixProvider codefix,
            bool includeConfigurationFixProviders = false,
            bool throwExceptionInFixerCreation = false,
            TestHostDocument? additionalDocument = null)
            => ServiceSetup(ImmutableArray.Create(codefix), includeConfigurationFixProviders, throwExceptionInFixerCreation, additionalDocument);
 
        private static (TestWorkspace workspace, DiagnosticAnalyzerService analyzerService, CodeFixService codeFixService, IErrorLoggerService errorLogger) ServiceSetup(
            ImmutableArray<CodeFixProvider> codefixers,
            bool includeConfigurationFixProviders = false,
            bool throwExceptionInFixerCreation = false,
            TestHostDocument? additionalDocument = null)
        {
            var fixers = codefixers.Select(codefix =>
                new Lazy<CodeFixProvider, CodeChangeProviderMetadata>(
                () => throwExceptionInFixerCreation ? throw new Exception() : codefix,
                new CodeChangeProviderMetadata("Test", languages: LanguageNames.CSharp)));
 
            var code = @"class Program { }";
 
            var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithMockDiagnosticUpdateSourceRegistrationService, openDocuments: true);
            if (additionalDocument != null)
            {
                workspace.Projects.Single().AddAdditionalDocument(additionalDocument);
                workspace.AdditionalDocuments.Add(additionalDocument);
                workspace.OnAdditionalDocumentAdded(additionalDocument.ToDocumentInfo());
            }
 
            var analyzerReference = new TestAnalyzerReferenceByLanguage(DiagnosticExtensions.GetCompilerDiagnosticAnalyzersMap());
            workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(new[] { analyzerReference }));
 
            Assert.IsType<MockDiagnosticUpdateSourceRegistrationService>(workspace.GetService<IDiagnosticUpdateSourceRegistrationService>());
            var diagnosticService = Assert.IsType<DiagnosticAnalyzerService>(workspace.GetService<IDiagnosticAnalyzerService>());
            var logger = SpecializedCollections.SingletonEnumerable(new Lazy<IErrorLoggerService>(() => new TestErrorLogger()));
            var errorLogger = logger.First().Value;
 
            var configurationFixProviders = includeConfigurationFixProviders
                ? workspace.ExportProvider.GetExports<IConfigurationFixProvider, CodeChangeProviderMetadata>()
                : SpecializedCollections.EmptyEnumerable<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>>();
 
            var fixService = new CodeFixService(
                diagnosticService,
                logger,
                fixers,
                configurationFixProviders);
 
            return (workspace, diagnosticService, fixService, errorLogger);
        }
 
        private static void GetDocumentAndExtensionManager(
            DiagnosticAnalyzerService diagnosticService,
            TestWorkspace workspace,
            out TextDocument document,
            out EditorLayerExtensionManager.ExtensionManager extensionManager,
            MockAnalyzerReference? analyzerReference = null,
            TextDocumentKind documentKind = TextDocumentKind.Document)
        {
            var incrementalAnalyzer = (IIncrementalAnalyzerProvider)diagnosticService;
 
            // register diagnostic engine to solution crawler
            _ = incrementalAnalyzer.CreateIncrementalAnalyzer(workspace);
 
            var reference = analyzerReference ?? new MockAnalyzerReference();
            var project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(reference);
            document = documentKind switch
            {
                TextDocumentKind.Document => project.Documents.Single(),
                TextDocumentKind.AdditionalDocument => project.AdditionalDocuments.Single(),
                TextDocumentKind.AnalyzerConfigDocument => project.AnalyzerConfigDocuments.Single(),
                _ => throw new NotImplementedException(),
            };
            extensionManager = (EditorLayerExtensionManager.ExtensionManager)document.Project.Solution.Services.GetRequiredService<IExtensionManager>();
        }
 
        private static IEnumerable<Lazy<CodeFixProvider, CodeChangeProviderMetadata>> CreateFixers()
        {
            return SpecializedCollections.SingletonEnumerable(
                new Lazy<CodeFixProvider, CodeChangeProviderMetadata>(() => new MockFixer(), new CodeChangeProviderMetadata("Test", languages: LanguageNames.CSharp)));
        }
 
        internal class MockFixer : CodeFixProvider
        {
            public const string Id = "MyDiagnostic";
            public bool Called;
            public int ContextDiagnosticsCount;
 
            public sealed override ImmutableArray<string> FixableDiagnosticIds
            {
                get { return ImmutableArray.Create(Id); }
            }
 
            public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
            {
                Called = true;
                ContextDiagnosticsCount = context.Diagnostics.Length;
                return Task.CompletedTask;
            }
        }
 
        private class MockAnalyzerReference : AnalyzerReference, ICodeFixProviderFactory
        {
            public readonly ImmutableArray<CodeFixProvider> Fixers;
            public readonly ImmutableArray<DiagnosticAnalyzer> Analyzers;
 
            private static readonly ImmutableArray<CodeFixProvider> s_defaultFixers = ImmutableArray.Create<CodeFixProvider>(new MockFixer());
            private static readonly ImmutableArray<DiagnosticAnalyzer> s_defaultAnalyzers = ImmutableArray.Create<DiagnosticAnalyzer>(new MockDiagnosticAnalyzer());
 
            public MockAnalyzerReference(ImmutableArray<CodeFixProvider> fixers, ImmutableArray<DiagnosticAnalyzer> analyzers)
            {
                Fixers = fixers;
                Analyzers = analyzers;
            }
 
            public MockAnalyzerReference(CodeFixProvider? fixer, ImmutableArray<DiagnosticAnalyzer> analyzers)
                : this(fixer != null ? ImmutableArray.Create(fixer) : ImmutableArray<CodeFixProvider>.Empty,
                       analyzers)
            {
            }
 
            public MockAnalyzerReference()
                : this(s_defaultFixers, s_defaultAnalyzers)
            {
            }
 
            public MockAnalyzerReference(CodeFixProvider? fixer)
                : this(fixer, s_defaultAnalyzers)
            {
            }
 
            public override string Display
            {
                get
                {
                    return "MockAnalyzerReference";
                }
            }
 
            public override string FullPath
            {
                get
                {
                    return string.Empty;
                }
            }
 
            public override object Id
            {
                get
                {
                    return "MockAnalyzerReference";
                }
            }
 
            public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(string language)
                => Analyzers;
 
            public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
                => ImmutableArray<DiagnosticAnalyzer>.Empty;
 
            public ImmutableArray<CodeFixProvider> GetFixers()
                => Fixers;
 
            private static ImmutableArray<DiagnosticDescriptor> CreateSupportedDiagnostics(ImmutableArray<(string id, string category)> reportedDiagnosticIdsWithCategories)
            {
                var builder = ArrayBuilder<DiagnosticDescriptor>.GetInstance();
                foreach (var (diagnosticId, category) in reportedDiagnosticIdsWithCategories)
                {
                    var descriptor = new DiagnosticDescriptor(diagnosticId, "MockDiagnostic", "MockDiagnostic", category, DiagnosticSeverity.Warning, isEnabledByDefault: true);
                    builder.Add(descriptor);
                }
 
                return builder.ToImmutableAndFree();
            }
 
            public class MockDiagnosticAnalyzer : DiagnosticAnalyzer
            {
                public MockDiagnosticAnalyzer(ImmutableArray<(string id, string category)> reportedDiagnosticIdsWithCategories)
                    => SupportedDiagnostics = CreateSupportedDiagnostics(reportedDiagnosticIdsWithCategories);
 
                public MockDiagnosticAnalyzer(string diagnosticId, string category)
                    : this(ImmutableArray.Create((diagnosticId, category)))
                {
                }
 
                public MockDiagnosticAnalyzer(ImmutableArray<string> reportedDiagnosticIds)
                    : this(reportedDiagnosticIds.SelectAsArray(id => (id, "InternalCategory")))
                {
                }
 
                public MockDiagnosticAnalyzer()
                    : this(ImmutableArray.Create(MockFixer.Id))
                {
                }
 
                public bool ReceivedCallback { get; private set; }
 
                public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
 
                public override void Initialize(AnalysisContext context)
                {
                    context.RegisterSyntaxTreeAction(c =>
                    {
                        this.ReceivedCallback = true;
 
                        foreach (var descriptor in SupportedDiagnostics)
                        {
                            c.ReportDiagnostic(Diagnostic.Create(descriptor, c.Tree.GetLocation(TextSpan.FromBounds(0, 0))));
                        }
                    });
                }
            }
 
            public class MockDocumentDiagnosticAnalyzer : DocumentDiagnosticAnalyzer
            {
                public MockDocumentDiagnosticAnalyzer(ImmutableArray<(string id, string category)> reportedDiagnosticIdsWithCategories)
                    => SupportedDiagnostics = CreateSupportedDiagnostics(reportedDiagnosticIdsWithCategories);
 
                public MockDocumentDiagnosticAnalyzer(ImmutableArray<string> reportedDiagnosticIds)
                    : this(reportedDiagnosticIds.SelectAsArray(id => (id, "InternalCategory")))
                {
                }
 
                public MockDocumentDiagnosticAnalyzer()
                    : this(ImmutableArray.Create(MockFixer.Id))
                {
                }
 
                public bool ReceivedCallback { get; private set; }
 
                public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
 
                public override Task<ImmutableArray<Diagnostic>> AnalyzeSyntaxAsync(Document document, CancellationToken cancellationToken)
                {
                    ReceivedCallback = true;
                    return Task.FromResult(ImmutableArray<Diagnostic>.Empty);
                }
 
                public override Task<ImmutableArray<Diagnostic>> AnalyzeSemanticsAsync(Document document, CancellationToken cancellationToken)
                {
                    ReceivedCallback = true;
                    return Task.FromResult(ImmutableArray<Diagnostic>.Empty);
                }
            }
        }
 
        internal class TestErrorLogger : IErrorLoggerService
        {
            public Dictionary<string, string> Messages = new Dictionary<string, string>();
 
            public void LogException(object source, Exception exception)
                => Messages.Add(source.GetType().Name, ToLogFormat(exception));
 
            private static string ToLogFormat(Exception exception)
                => exception.Message + Environment.NewLine + exception.StackTrace;
        }
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/18818")]
        public async Task TestNuGetAndVsixCodeFixersAsync()
        {
            // No NuGet or VSIX code fix provider
            // Verify no code action registered
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: null,
                expectedNuGetFixerCodeActionWasRegistered: false,
                vsixFixer: null,
                expectedVsixFixerCodeActionWasRegistered: false);
 
            // Only NuGet code fix provider
            // Verify only NuGet fixer's code action registered
            var fixableDiagnosticIds = ImmutableArray.Create(MockFixer.Id);
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: new NuGetCodeFixProvider(fixableDiagnosticIds),
                expectedNuGetFixerCodeActionWasRegistered: true,
                vsixFixer: null,
                expectedVsixFixerCodeActionWasRegistered: false);
 
            // Only Vsix code fix provider
            // Verify only Vsix fixer's code action registered
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: null,
                expectedNuGetFixerCodeActionWasRegistered: false,
                vsixFixer: new VsixCodeFixProvider(fixableDiagnosticIds),
                expectedVsixFixerCodeActionWasRegistered: true);
 
            // Both NuGet and Vsix code fix provider
            // Verify only NuGet fixer's code action registered
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: new NuGetCodeFixProvider(fixableDiagnosticIds),
                expectedNuGetFixerCodeActionWasRegistered: true,
                vsixFixer: new VsixCodeFixProvider(fixableDiagnosticIds),
                expectedVsixFixerCodeActionWasRegistered: false);
        }
 
        private static async Task TestNuGetAndVsixCodeFixersCoreAsync(
            NuGetCodeFixProvider? nugetFixer,
            bool expectedNuGetFixerCodeActionWasRegistered,
            VsixCodeFixProvider? vsixFixer,
            bool expectedVsixFixerCodeActionWasRegistered,
            MockAnalyzerReference.MockDiagnosticAnalyzer? diagnosticAnalyzer = null)
        {
            var fixes = await GetNuGetAndVsixCodeFixersCoreAsync(nugetFixer, vsixFixer, diagnosticAnalyzer);
 
            var fixTitles = fixes.SelectMany(fixCollection => fixCollection.Fixes).Select(f => f.Action.Title).ToHashSet();
            Assert.Equal(expectedNuGetFixerCodeActionWasRegistered, fixTitles.Contains(nameof(NuGetCodeFixProvider)));
            Assert.Equal(expectedVsixFixerCodeActionWasRegistered, fixTitles.Contains(nameof(VsixCodeFixProvider)));
        }
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/18818")]
        public async Task TestNuGetAndVsixCodeFixersWithMultipleFixableDiagnosticIdsAsync()
        {
            const string id1 = "ID1";
            const string id2 = "ID2";
            var reportedDiagnosticIds = ImmutableArray.Create(id1, id2);
            var diagnosticAnalyzer = new MockAnalyzerReference.MockDiagnosticAnalyzer(reportedDiagnosticIds);
 
            // Only NuGet code fix provider which fixes both reported diagnostic IDs.
            // Verify only NuGet fixer's code actions registered and they fix all IDs.
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: new NuGetCodeFixProvider(reportedDiagnosticIds),
                expectedDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer: reportedDiagnosticIds,
                vsixFixer: null,
                expectedDiagnosticIdsWithRegisteredCodeActionsByVsixFixer: ImmutableArray<string>.Empty,
                diagnosticAnalyzer);
 
            // Only Vsix code fix provider which fixes both reported diagnostic IDs.
            // Verify only Vsix fixer's code action registered and they fix all IDs.
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: null,
                expectedDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer: ImmutableArray<string>.Empty,
                vsixFixer: new VsixCodeFixProvider(reportedDiagnosticIds),
                expectedDiagnosticIdsWithRegisteredCodeActionsByVsixFixer: reportedDiagnosticIds,
                diagnosticAnalyzer);
 
            // Both NuGet and Vsix code fix provider register same fixable IDs.
            // Verify only NuGet fixer's code actions registered.
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: new NuGetCodeFixProvider(reportedDiagnosticIds),
                expectedDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer: reportedDiagnosticIds,
                vsixFixer: new VsixCodeFixProvider(reportedDiagnosticIds),
                expectedDiagnosticIdsWithRegisteredCodeActionsByVsixFixer: ImmutableArray<string>.Empty,
                diagnosticAnalyzer);
 
            // Both NuGet and Vsix code fix provider register different fixable IDs.
            // Verify both NuGet and Vsix fixer's code actions registered.
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: new NuGetCodeFixProvider(ImmutableArray.Create(id1)),
                expectedDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer: ImmutableArray.Create(id1),
                vsixFixer: new VsixCodeFixProvider(ImmutableArray.Create(id2)),
                expectedDiagnosticIdsWithRegisteredCodeActionsByVsixFixer: ImmutableArray.Create(id2),
                diagnosticAnalyzer);
 
            // NuGet code fix provider registers subset of Vsix code fix provider fixable IDs.
            // Verify both NuGet and Vsix fixer's code actions registered,
            // there are no duplicates and NuGet ones are preferred for duplicates.
            await TestNuGetAndVsixCodeFixersCoreAsync(
                nugetFixer: new NuGetCodeFixProvider(ImmutableArray.Create(id1)),
                expectedDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer: ImmutableArray.Create(id1),
                vsixFixer: new VsixCodeFixProvider(reportedDiagnosticIds),
                expectedDiagnosticIdsWithRegisteredCodeActionsByVsixFixer: ImmutableArray.Create(id2),
                diagnosticAnalyzer);
        }
 
        private static async Task TestNuGetAndVsixCodeFixersCoreAsync(
            NuGetCodeFixProvider? nugetFixer,
            ImmutableArray<string> expectedDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer,
            VsixCodeFixProvider? vsixFixer,
            ImmutableArray<string> expectedDiagnosticIdsWithRegisteredCodeActionsByVsixFixer,
            MockAnalyzerReference.MockDiagnosticAnalyzer diagnosticAnalyzer)
        {
            var fixes = (await GetNuGetAndVsixCodeFixersCoreAsync(nugetFixer, vsixFixer, diagnosticAnalyzer))
                .SelectMany(fixCollection => fixCollection.Fixes);
 
            var nugetFixerRegisteredActions = fixes.Where(f => f.Action.Title == nameof(NuGetCodeFixProvider));
            var actualDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer = nugetFixerRegisteredActions.SelectMany(a => a.Diagnostics).Select(d => d.Id);
            Assert.True(actualDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer.SetEquals(expectedDiagnosticIdsWithRegisteredCodeActionsByNuGetFixer));
 
            var vsixFixerRegisteredActions = fixes.Where(f => f.Action.Title == nameof(VsixCodeFixProvider));
            var actualDiagnosticIdsWithRegisteredCodeActionsByVsixFixer = vsixFixerRegisteredActions.SelectMany(a => a.Diagnostics).Select(d => d.Id);
            Assert.True(actualDiagnosticIdsWithRegisteredCodeActionsByVsixFixer.SetEquals(expectedDiagnosticIdsWithRegisteredCodeActionsByVsixFixer));
        }
 
        private static async Task<ImmutableArray<CodeFixCollection>> GetNuGetAndVsixCodeFixersCoreAsync(
            NuGetCodeFixProvider? nugetFixer,
            VsixCodeFixProvider? vsixFixer,
            MockAnalyzerReference.MockDiagnosticAnalyzer? diagnosticAnalyzer = null)
        {
            var code = @"class C { }";
 
            var vsixFixers = vsixFixer != null
                ? SpecializedCollections.SingletonEnumerable(new Lazy<CodeFixProvider, CodeChangeProviderMetadata>(() => vsixFixer, new CodeChangeProviderMetadata(name: nameof(VsixCodeFixProvider), languages: LanguageNames.CSharp)))
                : SpecializedCollections.EmptyEnumerable<Lazy<CodeFixProvider, CodeChangeProviderMetadata>>();
 
            using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithMockDiagnosticUpdateSourceRegistrationService, openDocuments: true);
 
            Assert.IsType<MockDiagnosticUpdateSourceRegistrationService>(workspace.GetService<IDiagnosticUpdateSourceRegistrationService>());
            var diagnosticService = Assert.IsType<DiagnosticAnalyzerService>(workspace.GetService<IDiagnosticAnalyzerService>());
 
            var logger = SpecializedCollections.SingletonEnumerable(new Lazy<IErrorLoggerService>(() => workspace.Services.GetRequiredService<IErrorLoggerService>()));
            var fixService = new CodeFixService(
                diagnosticService, logger, vsixFixers, SpecializedCollections.EmptyEnumerable<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>>());
 
            var incrementalAnalyzer = (IIncrementalAnalyzerProvider)diagnosticService;
 
            // register diagnostic engine to solution crawler
            var analyzer = incrementalAnalyzer.CreateIncrementalAnalyzer(workspace);
 
            diagnosticAnalyzer ??= new MockAnalyzerReference.MockDiagnosticAnalyzer();
            var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(diagnosticAnalyzer);
            var reference = new MockAnalyzerReference(nugetFixer, analyzers);
            var project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(reference);
 
            var document = project.Documents.Single();
 
            return await fixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0), CodeActionOptions.DefaultProvider, isBlocking: false, CancellationToken.None);
        }
 
        private sealed class NuGetCodeFixProvider : AbstractNuGetOrVsixCodeFixProvider
        {
            public NuGetCodeFixProvider(ImmutableArray<string> fixableDiagnsoticIds)
                : base(fixableDiagnsoticIds, nameof(NuGetCodeFixProvider))
            {
            }
        }
 
        private sealed class VsixCodeFixProvider : AbstractNuGetOrVsixCodeFixProvider
        {
            public VsixCodeFixProvider(ImmutableArray<string> fixableDiagnsoticIds)
                : base(fixableDiagnsoticIds, nameof(VsixCodeFixProvider))
            {
            }
        }
 
        private abstract class AbstractNuGetOrVsixCodeFixProvider : CodeFixProvider
        {
            private readonly string _name;
 
            protected AbstractNuGetOrVsixCodeFixProvider(ImmutableArray<string> fixableDiagnsoticIds, string name)
            {
                FixableDiagnosticIds = fixableDiagnsoticIds;
                _name = name;
            }
 
            public override ImmutableArray<string> FixableDiagnosticIds { get; }
 
            public override Task RegisterCodeFixesAsync(CodeFixContext context)
            {
                var fixableDiagnostics = context.Diagnostics.WhereAsArray(d => FixableDiagnosticIds.Contains(d.Id));
                context.RegisterCodeFix(CodeAction.Create(_name, ct => Task.FromResult(context.Document)), fixableDiagnostics);
                return Task.CompletedTask;
            }
        }
 
        [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/44553")]
        [InlineData(null)]
        [InlineData("CodeFixProviderWithDuplicateEquivalenceKeyActions")]
        public async Task TestRegisteredCodeActionsWithSameEquivalenceKey(string? equivalenceKey)
        {
            var diagnosticId = "ID1";
            var analyzer = new MockAnalyzerReference.MockDiagnosticAnalyzer(ImmutableArray.Create(diagnosticId));
            var fixer = new CodeFixProviderWithDuplicateEquivalenceKeyActions(diagnosticId, equivalenceKey);
 
            // Verify multiple code actions registered with same equivalence key are not de-duped.
            var fixes = (await GetAddedFixesAsync(fixer, analyzer)).SelectMany(fixCollection => fixCollection.Fixes).ToList();
            Assert.Equal(2, fixes.Count);
        }
 
        private sealed class CodeFixProviderWithDuplicateEquivalenceKeyActions : CodeFixProvider
        {
            private readonly string _diagnosticId;
            private readonly string? _equivalenceKey;
 
            public CodeFixProviderWithDuplicateEquivalenceKeyActions(string diagnosticId, string? equivalenceKey)
            {
                _diagnosticId = diagnosticId;
                _equivalenceKey = equivalenceKey;
            }
 
            public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(_diagnosticId);
 
            public override Task RegisterCodeFixesAsync(CodeFixContext context)
            {
                // Register duplicate code actions with same equivalence key, but different title.
                RegisterCodeFix(context, titleSuffix: "1");
                RegisterCodeFix(context, titleSuffix: "2");
 
                return Task.CompletedTask;
            }
 
            private void RegisterCodeFix(CodeFixContext context, string titleSuffix)
            {
                context.RegisterCodeFix(
                    CodeAction.Create(
                        nameof(CodeFixProviderWithDuplicateEquivalenceKeyActions) + titleSuffix,
                        ct => Task.FromResult(context.Document),
                        _equivalenceKey),
                    context.Diagnostics);
            }
        }
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/62877")]
        public async Task TestAdditionalDocumentCodeFixAsync()
        {
            var analyzer = new AdditionalFileAnalyzer();
            var fixer1 = new AdditionalFileFixerWithDocumentKindsAndExtensions();
            var fixer2 = new AdditionalFileFixerWithDocumentKinds();
            var fixer3 = new AdditionalFileFixerWithDocumentExtensions();
            var fixer4 = new AdditionalFileFixerWithoutDocumentKindsAndExtensions();
            var fixers = ImmutableArray.Create<CodeFixProvider>(fixer1, fixer2, fixer3, fixer4);
            var analyzerReference = new MockAnalyzerReference(fixers, ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
 
            // Verify available code fixes for .txt additional document
            var tuple = ServiceSetup(fixers, additionalDocument: new TestHostDocument("Additional Document", filePath: "test.txt"));
            using var workspace = tuple.workspace;
            GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var txtDocument, out var extensionManager, analyzerReference, documentKind: TextDocumentKind.AdditionalDocument);
            var txtDocumentCodeFixes = await tuple.codeFixService.GetFixesAsync(txtDocument, TextSpan.FromBounds(0, 1), CodeActionOptions.DefaultProvider, isBlocking: false, CancellationToken.None);
            Assert.Equal(2, txtDocumentCodeFixes.Length);
            var txtDocumentCodeFixTitles = txtDocumentCodeFixes.Select(s => s.Fixes.Single().Action.Title).ToImmutableArray();
            Assert.Contains(fixer1.Title, txtDocumentCodeFixTitles);
            Assert.Contains(fixer2.Title, txtDocumentCodeFixTitles);
 
            // Verify code fix application
            var codeAction = txtDocumentCodeFixes.Single(s => s.Fixes.Single().Action.Title == fixer1.Title).Fixes.Single().Action;
            var solution = await codeAction.GetChangedSolutionInternalAsync(txtDocument.Project.Solution);
            var changedtxtDocument = solution!.Projects.Single().AdditionalDocuments.Single(t => t.Id == txtDocument.Id);
            Assert.Equal("Additional Document", txtDocument.GetTextSynchronously(CancellationToken.None).ToString());
            Assert.Equal($"Additional Document{fixer1.Title}", changedtxtDocument.GetTextSynchronously(CancellationToken.None).ToString());
 
            // Verify available code fixes for .log additional document
            tuple = ServiceSetup(fixers, additionalDocument: new TestHostDocument("Additional Document", filePath: "test.log"));
            using var workspace2 = tuple.workspace;
            GetDocumentAndExtensionManager(tuple.analyzerService, workspace2, out var logDocument, out extensionManager, analyzerReference, documentKind: TextDocumentKind.AdditionalDocument);
            var logDocumentCodeFixes = await tuple.codeFixService.GetFixesAsync(logDocument, TextSpan.FromBounds(0, 1), CodeActionOptions.DefaultProvider, isBlocking: false, CancellationToken.None);
            var logDocumentCodeFix = Assert.Single(logDocumentCodeFixes);
            var logDocumentCodeFixTitle = logDocumentCodeFix.Fixes.Single().Action.Title;
            Assert.Equal(fixer2.Title, logDocumentCodeFixTitle);
        }
 
        [DiagnosticAnalyzer(LanguageNames.CSharp)]
        internal sealed class AdditionalFileAnalyzer : DiagnosticAnalyzer
        {
            public const string DiagnosticId = "AFA0001";
            private readonly DiagnosticDescriptor _descriptor = new(DiagnosticId, "AdditionalFileAnalyzer", "AdditionalFileAnalyzer", "AdditionalFileAnalyzer", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_descriptor);
 
            public override void Initialize(AnalysisContext context)
            {
                context.EnableConcurrentExecution();
                context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 
                context.RegisterAdditionalFileAction(context =>
                {
                    var text = context.AdditionalFile.GetText(context.CancellationToken);
                    if (text == null || text.Lines.Count == 0)
                        return;
                    var line = text.Lines[0];
                    var span = new TextSpan(line.Start, line.End);
                    var location = Location.Create(context.AdditionalFile.Path, span, text.Lines.GetLinePositionSpan(span));
                    context.ReportDiagnostic(Diagnostic.Create(_descriptor, location));
                });
            }
        }
 
        internal abstract class AbstractAdditionalFileCodeFixProvider : CodeFixProvider
        {
            public string Title { get; }
 
            protected AbstractAdditionalFileCodeFixProvider(string title)
                => Title = title;
 
            public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AdditionalFileAnalyzer.DiagnosticId);
 
            public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
            {
                context.RegisterCodeFix(CodeAction.Create(Title,
                    createChangedSolution: async ct =>
                    {
                        var document = context.TextDocument;
                        var text = await document.GetTextAsync(ct).ConfigureAwait(false);
                        var newText = SourceText.From(text.ToString() + Title);
                        return document.Project.Solution.WithAdditionalDocumentText(document.Id, newText);
                    },
                    equivalenceKey: Title),
                    context.Diagnostics[0]);
 
                return Task.CompletedTask;
            }
        }
 
#pragma warning disable RS0034 // Exported parts should be marked with 'ImportingConstructorAttribute'
        [ExportCodeFixProvider(
            LanguageNames.CSharp,
            DocumentKinds = new[] { nameof(TextDocumentKind.AdditionalDocument) },
            DocumentExtensions = new[] { ".txt" })]
        [Shared]
        internal sealed class AdditionalFileFixerWithDocumentKindsAndExtensions : AbstractAdditionalFileCodeFixProvider
        {
            public AdditionalFileFixerWithDocumentKindsAndExtensions() : base(nameof(AdditionalFileFixerWithDocumentKindsAndExtensions)) { }
        }
 
        [ExportCodeFixProvider(
            LanguageNames.CSharp,
            DocumentKinds = new[] { nameof(TextDocumentKind.AdditionalDocument) })]
        [Shared]
        internal sealed class AdditionalFileFixerWithDocumentKinds : AbstractAdditionalFileCodeFixProvider
        {
            public AdditionalFileFixerWithDocumentKinds() : base(nameof(AdditionalFileFixerWithDocumentKinds)) { }
        }
 
        [ExportCodeFixProvider(
            LanguageNames.CSharp,
            DocumentExtensions = new[] { ".txt" })]
        [Shared]
        internal sealed class AdditionalFileFixerWithDocumentExtensions : AbstractAdditionalFileCodeFixProvider
        {
            public AdditionalFileFixerWithDocumentExtensions() : base(nameof(AdditionalFileFixerWithDocumentExtensions)) { }
        }
 
        [ExportCodeFixProvider(LanguageNames.CSharp)]
        [Shared]
        internal sealed class AdditionalFileFixerWithoutDocumentKindsAndExtensions : AbstractAdditionalFileCodeFixProvider
        {
            public AdditionalFileFixerWithoutDocumentKindsAndExtensions() : base(nameof(AdditionalFileFixerWithoutDocumentKindsAndExtensions)) { }
        }
#pragma warning restore RS0034 // Exported parts should be marked with 'ImportingConstructorAttribute'
    }
}