File: CodeFixes\CodeFixServiceTests.cs
Web Access
Project: src\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.Diagnostics.EngineV2;
using Microsoft.CodeAnalysis.Editor.Implementation.Suggestions;
using Microsoft.CodeAnalysis.ErrorLogger;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
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;
 
    [Fact]
    public async Task TestGetFirstDiagnosticWithFixAsync()
    {
        var fixers = CreateFixers();
        var code = @"
    a
";
        using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithMockDiagnosticUpdateSourceRegistrationService, openDocuments: true);
 
        var diagnosticService = Assert.IsType<DiagnosticAnalyzerService>(workspace.GetService<IDiagnosticAnalyzerService>());
 
        var analyzerReference = new TestAnalyzerReferenceByLanguage(DiagnosticExtensions.GetCompilerDiagnosticAnalyzersMap());
        workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences([analyzerReference]));
 
        var logger = SpecializedCollections.SingletonEnumerable(new Lazy<IErrorLoggerService>(() => workspace.Services.GetRequiredService<IErrorLoggerService>()));
        var fixService = new CodeFixService(
            diagnosticService, logger, fixers, configurationProviders: []);
 
        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), new DefaultCodeActionRequestPriorityProvider(), 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), 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), CancellationToken.None);
        var codeActions = fixCollections.SelectManyAsArray(c => c.Fixes.Select(f => f.Action));
        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),
            priorityProvider: new DefaultCodeActionRequestPriorityProvider(CodeActionRequestPriority.Default),
            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),
            priorityProvider: new DefaultCodeActionRequestPriorityProvider(CodeActionRequestPriority.Lowest),
            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),
            priorityProvider: new DefaultCodeActionRequestPriorityProvider(CodeActionRequestPriority.Default),
            cancellationToken: CancellationToken.None);
        Assert.True(documentDiagnosticAnalyzer.ReceivedCallback);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67354")]
    public async Task TestGetFixesAsyncForGeneratorDiagnosticAsync()
    {
        // We have a special GeneratorDiagnosticsPlaceholderAnalyzer that report 0 SupportedDiagnostics.
        // We need to ensure that we don't skip this special analyzer
        // when computing the diagnostics/code fixes for "Normal" priority bucket, which
        // normally only execute those analyzers which report at least one fixable supported diagnostic.
        // Note that this special placeholder analyzer instance is always included for the project,
        // we do not need to include it in the passed in analyzers.
        Assert.Empty(GeneratorDiagnosticsPlaceholderAnalyzer.Instance.SupportedDiagnostics);
 
        var analyzers = ImmutableArray<DiagnosticAnalyzer>.Empty;
        var generator = new MockAnalyzerReference.MockGenerator();
        var generators = ImmutableArray.Create<ISourceGenerator>(generator);
        var fixTitle = "Fix Title";
        var codeFix = new MockFixer(fixTitle);
        var codeFixes = ImmutableArray.Create<CodeFixProvider>(codeFix);
        var analyzerReference = new MockAnalyzerReference(codeFixes, analyzers, generators);
 
        var tuple = ServiceSetup(codeFix, includeConfigurationFixProviders: false);
        using var workspace = tuple.workspace;
        GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document, out var extensionManager, analyzerReference);
 
        Assert.False(codeFix.Called);
        var fixCollectionSet = await tuple.codeFixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0),
            priorityProvider: new DefaultCodeActionRequestPriorityProvider(CodeActionRequestPriority.Default),
            cancellationToken: CancellationToken.None);
        Assert.True(codeFix.Called);
        var fixCollection = Assert.Single(fixCollectionSet);
        Assert.Equal(MockFixer.Id, fixCollection.FirstDiagnostic.Id);
        var fix = Assert.Single(fixCollection.Fixes);
        Assert.Equal(fixTitle, fix.Action.Title);
    }
 
    [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 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), 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), new DefaultCodeActionRequestPriorityProvider(), CancellationToken.None);
        Assert.True(extensionManager.IsDisabled(codefix));
        Assert.False(extensionManager.IsIgnored(codefix));
        Assert.True(errorReported);
    }
 
    private static (EditorTestWorkspace workspace, DiagnosticAnalyzerService analyzerService, CodeFixService codeFixService, IErrorLoggerService errorLogger) ServiceSetup(
        CodeFixProvider codefix,
        bool includeConfigurationFixProviders = false,
        bool throwExceptionInFixerCreation = false,
        EditorTestHostDocument? additionalDocument = null,
        string code = "class Program { }")
        => ServiceSetup(ImmutableArray.Create(codefix), includeConfigurationFixProviders, throwExceptionInFixerCreation, additionalDocument, code);
 
    private static (EditorTestWorkspace workspace, DiagnosticAnalyzerService analyzerService, CodeFixService codeFixService, IErrorLoggerService errorLogger) ServiceSetup(
        ImmutableArray<CodeFixProvider> codefixers,
        bool includeConfigurationFixProviders = false,
        bool throwExceptionInFixerCreation = false,
        EditorTestHostDocument? additionalDocument = null,
        string code = "class Program { }")
    {
        var fixers = codefixers.Select(codefix =>
            new Lazy<CodeFixProvider, CodeChangeProviderMetadata>(
            () => throwExceptionInFixerCreation ? throw new Exception() : codefix,
            new CodeChangeProviderMetadata("Test", languages: LanguageNames.CSharp)));
 
        var workspace = EditorTestWorkspace.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([analyzerReference]));
 
        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>()
            : [];
 
        var fixService = new CodeFixService(
            diagnosticService,
            logger,
            fixers,
            configurationFixProviders);
 
        return (workspace, diagnosticService, fixService, errorLogger);
    }
 
    private static void GetDocumentAndExtensionManager(
        DiagnosticAnalyzerService diagnosticService,
        EditorTestWorkspace workspace,
        out TextDocument document,
        out EditorLayerExtensionManager.ExtensionManager extensionManager,
        MockAnalyzerReference? analyzerReference = null,
        TextDocumentKind documentKind = TextDocumentKind.Document)
            => GetDocumentAndExtensionManager(diagnosticService, workspace, out document, out extensionManager, out _, analyzerReference, documentKind);
 
    private static void GetDocumentAndExtensionManager(
        DiagnosticAnalyzerService diagnosticService,
        EditorTestWorkspace workspace,
        out TextDocument document,
        out EditorLayerExtensionManager.ExtensionManager extensionManager,
        out DiagnosticIncrementalAnalyzer diagnosticIncrementalAnalyzer,
        MockAnalyzerReference? analyzerReference = null,
        TextDocumentKind documentKind = TextDocumentKind.Document)
    {
        // register diagnostic engine to solution crawler
        diagnosticIncrementalAnalyzer = diagnosticService.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()
        => [new Lazy<CodeFixProvider, CodeChangeProviderMetadata>(() => new MockFixer(), new CodeChangeProviderMetadata("Test", languages: LanguageNames.CSharp))];
 
    internal class MockFixer : CodeFixProvider
    {
        public const string Id = "MyDiagnostic";
        private readonly string? _registerFixWithTitle;
        public bool Called;
        public int ContextDiagnosticsCount;
 
        public MockFixer(string? registerFixWithTitle = null)
        {
            _registerFixWithTitle = registerFixWithTitle;
        }
 
        public sealed override ImmutableArray<string> FixableDiagnosticIds
        {
            get { return ImmutableArray.Create(Id); }
        }
 
        public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            Called = true;
            ContextDiagnosticsCount = context.Diagnostics.Length;
            if (_registerFixWithTitle != null)
            {
                context.RegisterCodeFix(
                    CodeAction.Create(
                        _registerFixWithTitle,
                        createChangedDocument: _ => Task.FromResult(context.Document)),
                    context.Diagnostics);
            }
 
            return Task.CompletedTask;
        }
    }
 
    private class MockAnalyzerReference : AnalyzerReference, ICodeFixProviderFactory
    {
        public readonly ImmutableArray<CodeFixProvider> Fixers;
        public readonly ImmutableArray<DiagnosticAnalyzer> Analyzers;
        public readonly ImmutableArray<ISourceGenerator> Generators;
 
        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, ImmutableArray<ISourceGenerator> generators)
        {
            Fixers = fixers;
            Analyzers = analyzers;
            Generators = generators;
        }
 
        public MockAnalyzerReference(ImmutableArray<CodeFixProvider> fixers, ImmutableArray<DiagnosticAnalyzer> analyzers)
            : this(fixers, analyzers, ImmutableArray<ISourceGenerator>.Empty)
        {
        }
 
        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 override ImmutableArray<ISourceGenerator> GetGenerators(string language)
            => Generators;
 
        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);
            }
        }
 
#pragma warning disable RS1042 // Do not implement
        public class MockGenerator : ISourceGenerator
#pragma warning restore RS1042 // Do not implement
        {
            private readonly DiagnosticDescriptor s_descriptor = new(MockFixer.Id, "Title", "Message", "Category", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
            public void Initialize(GeneratorInitializationContext context)
            {
            }
 
            public void Execute(GeneratorExecutionContext context)
            {
                foreach (var tree in context.Compilation.SyntaxTrees)
                {
                    context.ReportDiagnostic(Diagnostic.Create(s_descriptor, tree.GetLocation(new TextSpan(0, 1))));
                }
            }
        }
    }
 
    internal class TestErrorLogger : IErrorLoggerService
    {
        public Dictionary<string, string> Messages = [];
 
        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)))
            : [];
 
        using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithMockDiagnosticUpdateSourceRegistrationService, openDocuments: true);
 
        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, configurationProviders: []);
 
        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), 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 EditorTestHostDocument("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), 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, CodeAnalysisProgress.None);
        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 EditorTestHostDocument("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), 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 = [nameof(TextDocumentKind.AdditionalDocument)],
        DocumentExtensions = [".txt"])]
    [Shared]
    internal sealed class AdditionalFileFixerWithDocumentKindsAndExtensions : AbstractAdditionalFileCodeFixProvider
    {
        public AdditionalFileFixerWithDocumentKindsAndExtensions() : base(nameof(AdditionalFileFixerWithDocumentKindsAndExtensions)) { }
    }
 
    [ExportCodeFixProvider(
        LanguageNames.CSharp,
        DocumentKinds = [nameof(TextDocumentKind.AdditionalDocument)])]
    [Shared]
    internal sealed class AdditionalFileFixerWithDocumentKinds : AbstractAdditionalFileCodeFixProvider
    {
        public AdditionalFileFixerWithDocumentKinds() : base(nameof(AdditionalFileFixerWithDocumentKinds)) { }
    }
 
    [ExportCodeFixProvider(
        LanguageNames.CSharp,
        DocumentExtensions = [".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'
 
    [Theory, CombinatorialData]
    public async Task TestGetFixesWithDeprioritizedAnalyzerAsync(
        DeprioritizedAnalyzer.ActionKind actionKind,
        bool diagnosticOnFixLineInPriorSnapshot,
        bool editOnFixLine,
        bool addNewLineWithEdit)
    {
        // This test validates analyzer de-prioritization logic in diagnostic service for lightbulb code path.
        // Basically, we have a certain set of heuristics (detailed in the next comment below), under which an analyzer
        // which is deemed to be an expensive analyzer is moved down from 'Normal' priority code fix bucket to
        // 'Low' priority bucket to improve lightbulb performance. This test validates this logic by performing
        // the following steps:
        //  1. Use 2 snapshots of document, such that the analyzer has a reported diagnostic on the code fix trigger
        //     line in the earlier snapshot based on the flag 'diagnosticOnFixLineInPriorSnapshot'
        //  2. For the second snapshot, mimic whether or not background analysis has computed and cached full document
        //     diagnostics for the document based on the flag 'testWithCachedDiagnostics'.
        //  3. Apply an edit in the second snapshot on the code fix trigger line based on the flag 'editOnFixLine'.
        //     If this flag is false, edit is apply at a different line in the document.
        //  4. Add a new line edit in the second snapshot based on the flag 'addNewLineWithEdit'. This tests the part
        //     of the heuristic where we compare intersecting diagnostics across document snapshots only if both
        //     snapshots have the same number of lines.
 
        var expectDeprioritization = GetExpectDeprioritization(actionKind, diagnosticOnFixLineInPriorSnapshot, addNewLineWithEdit);
 
        var priorSnapshotFixLine = diagnosticOnFixLineInPriorSnapshot ? "int x1 = 0;" : "System.Console.WriteLine();";
        var code = $@"
#pragma warning disable CS0219
class C
{{
    void M()
    {{
        {priorSnapshotFixLine}
    }}
}}";
 
        var codeFix = new FixerForDeprioritizedAnalyzer();
        var analyzer = new DeprioritizedAnalyzer(actionKind);
        var analyzerReference = new MockAnalyzerReference(codeFix, ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
 
        var tuple = ServiceSetup(codeFix, code: code);
        using var workspace = tuple.workspace;
        GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document,
            out var extensionManager, out var diagnosticIncrementalAnalyzer, analyzerReference);
 
        var sourceDocument = (Document)document;
        var root = await sourceDocument.GetRequiredSyntaxRootAsync(CancellationToken.None);
        var testSpan = diagnosticOnFixLineInPriorSnapshot
            ? root.DescendantNodes().OfType<CodeAnalysis.CSharp.Syntax.VariableDeclarationSyntax>().First().Span
            : root.DescendantNodes().OfType<CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax>().First().Span;
 
        // Trigger background analysis to ensure analyzer diagnostics are computed and cached. 
        // We enable full solution analysis so the 'AnalyzeDocumentAsync' doesn't skip analysis based on whether the document is active/open.
        workspace.GlobalOptions.SetGlobalOption(SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, LanguageNames.CSharp, BackgroundAnalysisScope.FullSolution);
 
        await diagnosticIncrementalAnalyzer.ForceAnalyzeProjectAsync(sourceDocument.Project, CancellationToken.None);
        await VerifyCachedDiagnosticsAsync(sourceDocument, expectedCachedDiagnostic: diagnosticOnFixLineInPriorSnapshot, testSpan, diagnosticIncrementalAnalyzer);
 
        // Compute and apply code edit
        if (editOnFixLine)
        {
            code = code.Replace(priorSnapshotFixLine, "int x2 = 0;");
        }
        else
        {
            code += " // Comment at end of file";
        }
 
        if (addNewLineWithEdit)
        {
            // Add a new line at the start of the document to ensure the line with diagnostic in prior snapshot moved.
            code = Environment.NewLine + code;
        }
 
        sourceDocument = sourceDocument.WithText(SourceText.From(code));
        var appliedChanges = workspace.TryApplyChanges(sourceDocument.Project.Solution);
        Assert.True(appliedChanges);
        sourceDocument = workspace.CurrentSolution.Projects.Single().Documents.Single();
 
        root = await sourceDocument.GetRequiredSyntaxRootAsync(CancellationToken.None);
        var expectedNoFixes = !diagnosticOnFixLineInPriorSnapshot && !editOnFixLine;
        testSpan = !expectedNoFixes
            ? root.DescendantNodes().OfType<CodeAnalysis.CSharp.Syntax.VariableDeclarationSyntax>().First().Span
            : root.DescendantNodes().OfType<CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax>().First().Span;
 
        await diagnosticIncrementalAnalyzer.GetDiagnosticsForIdsAsync(
            sourceDocument.Project.Solution, sourceDocument.Project.Id, sourceDocument.Id, diagnosticIds: null, shouldIncludeAnalyzer: null, getDocuments: null,
            includeSuppressedDiagnostics: true, includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: true, CancellationToken.None);
        await diagnosticIncrementalAnalyzer.GetTestAccessor().TextDocumentOpenAsync(sourceDocument);
 
        var lowPriorityAnalyzerData = new SuggestedActionPriorityProvider.LowPriorityAnalyzersAndDiagnosticIds();
        var priorityProvider = new SuggestedActionPriorityProvider(CodeActionRequestPriority.Default, lowPriorityAnalyzerData);
        var normalPriFixes = await tuple.codeFixService.GetFixesAsync(sourceDocument, testSpan, priorityProvider, CancellationToken.None);
        priorityProvider = new SuggestedActionPriorityProvider(CodeActionRequestPriority.Low, lowPriorityAnalyzerData);
        var lowPriFixes = await tuple.codeFixService.GetFixesAsync(sourceDocument, testSpan, priorityProvider, CancellationToken.None);
 
        if (expectedNoFixes)
        {
            Assert.Empty(normalPriFixes);
            Assert.Empty(lowPriFixes);
            return;
        }
 
        CodeFixCollection expectedFixCollection;
        if (expectDeprioritization)
        {
            Assert.Empty(normalPriFixes);
            expectedFixCollection = Assert.Single(lowPriFixes);
            var lowPriorityAnalyzer = Assert.Single(lowPriorityAnalyzerData.Analyzers);
            Assert.Same(analyzer, lowPriorityAnalyzer);
            Assert.Equal(analyzer.SupportedDiagnostics.Select(d => d.Id), lowPriorityAnalyzerData.SupportedDiagnosticIds);
        }
        else
        {
            expectedFixCollection = Assert.Single(normalPriFixes);
            Assert.Empty(lowPriFixes);
            Assert.Empty(lowPriorityAnalyzerData.Analyzers);
            Assert.Empty(lowPriorityAnalyzerData.SupportedDiagnosticIds);
        }
 
        var fix = expectedFixCollection.Fixes.Single();
        Assert.Equal(FixerForDeprioritizedAnalyzer.Title, fix.Action.Title);
        return;
 
        static bool GetExpectDeprioritization(
            DeprioritizedAnalyzer.ActionKind actionKind,
            bool diagnosticOnFixLineInPriorSnapshot,
            bool addNewLineWithEdit)
        {
            // We expect de-prioritization of analyzer from 'Normal' to 'Low' bucket only if following conditions are met:
            //  1. We have an expensive analyzer that registers SymbolStart/End or SemanticModel actions, both of which have a broad analysis scope.
            //  2. Either of the below is true:
            //     a. We do not have an analyzer diagnostic reported in the prior document snapshot on the edited line OR
            //     b. Number of lines in the prior document snapshot differs from number of lines in the current document snapshot,
            //        i.e. we added a new line with the edit and 'addNewLineWithEdit = true'.
 
            // Condition 1
            if (actionKind is not (DeprioritizedAnalyzer.ActionKind.SymbolStartEnd or DeprioritizedAnalyzer.ActionKind.SemanticModel))
                return false;
 
            // Condition 2(a)
            if (!diagnosticOnFixLineInPriorSnapshot)
                return true;
 
            // Condition 2(b)
            return addNewLineWithEdit;
        }
 
        static async Task VerifyCachedDiagnosticsAsync(Document sourceDocument, bool expectedCachedDiagnostic, TextSpan testSpan, DiagnosticIncrementalAnalyzer diagnosticIncrementalAnalyzer)
        {
            var cachedDiagnostics = await diagnosticIncrementalAnalyzer.GetCachedDiagnosticsAsync(sourceDocument.Project.Solution, sourceDocument.Project.Id, sourceDocument.Id,
                includeSuppressedDiagnostics: false, includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: true, CancellationToken.None);
            if (!expectedCachedDiagnostic)
            {
                Assert.Empty(cachedDiagnostics);
            }
            else
            {
                var diagnostic = Assert.Single(cachedDiagnostics);
                Assert.Equal(DeprioritizedAnalyzer.Descriptor.Id, diagnostic.Id);
                var text = await sourceDocument.GetTextAsync();
                Assert.Equal(testSpan, diagnostic.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text));
            }
        }
    }
 
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public sealed class DeprioritizedAnalyzer : DiagnosticAnalyzer
    {
        public enum ActionKind
        {
            SymbolStartEnd,
            SemanticModel,
            Operation
        }
 
        public static readonly DiagnosticDescriptor Descriptor = new("ID0001", "Title", "Message", "Category", DiagnosticSeverity.Warning, isEnabledByDefault: true);
        private readonly ActionKind _actionKind;
 
        public DeprioritizedAnalyzer(ActionKind actionKind)
        {
            _actionKind = actionKind;
        }
 
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Descriptor);
 
        public override void Initialize(AnalysisContext context)
        {
            switch (_actionKind)
            {
                case ActionKind.SymbolStartEnd:
                    context.RegisterSymbolStartAction(context =>
                    {
                        var variableDeclarations = new HashSet<SyntaxNode>();
                        context.RegisterOperationAction(context
                            => variableDeclarations.Add(context.Operation.Syntax), OperationKind.VariableDeclaration);
                        context.RegisterSymbolEndAction(context =>
                        {
                            foreach (var decl in variableDeclarations)
                                context.ReportDiagnostic(Diagnostic.Create(Descriptor, decl.GetLocation()));
                        });
                    }, SymbolKind.NamedType);
                    break;
 
                case ActionKind.SemanticModel:
                    context.RegisterSemanticModelAction(context =>
                    {
                        var variableDeclarations = context.SemanticModel.SyntaxTree.GetRoot().DescendantNodes().OfType<CodeAnalysis.CSharp.Syntax.VariableDeclarationSyntax>();
                        foreach (var decl in variableDeclarations)
                            context.ReportDiagnostic(Diagnostic.Create(Descriptor, decl.GetLocation()));
                    });
                    break;
 
                case ActionKind.Operation:
                    context.RegisterOperationAction(context =>
                        context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Operation.Syntax.GetLocation())),
                        OperationKind.VariableDeclaration);
                    break;
            }
        }
    }
 
    private sealed class FixerForDeprioritizedAnalyzer : CodeFixProvider
    {
        public static readonly string Title = $"Fix {DeprioritizedAnalyzer.Descriptor.Id}";
        public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DeprioritizedAnalyzer.Descriptor.Id);
 
        public override Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            context.RegisterCodeFix(
                CodeAction.Create(Title,
                    createChangedDocument: _ => Task.FromResult(context.Document),
                    equivalenceKey: nameof(FixerForDeprioritizedAnalyzer)),
                context.Diagnostics);
            return Task.CompletedTask;
        }
    }
}