File: SolutionTests\SolutionWithSourceGeneratorTests.cs
Web Access
Project: src\src\Workspaces\CoreTest\Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.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.Immutable;
using System.Composition;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Remote.Testing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Test.Utilities.TestGenerators;
using Roslyn.Utilities;
using Xunit;
using static Microsoft.CodeAnalysis.UnitTests.SolutionTestHelpers;
using static Microsoft.CodeAnalysis.UnitTests.SolutionUtilities;
using static Microsoft.CodeAnalysis.UnitTests.WorkspaceTestUtilities;
 
namespace Microsoft.CodeAnalysis.UnitTests;
 
[UseExportProvider]
public sealed class SolutionWithSourceGeneratorTests : TestBase
{
    [Theory, CombinatorialData]
    public async Task SourceGeneratorBasedOnAdditionalFileGeneratesSyntaxTrees(
        bool fetchCompilationBeforeAddingAdditionalFile, TestHost testHost)
    {
        // This test is just the sanity test to make sure generators work at all. There's not a special scenario being
        // tested.
 
        var generatedFilesOutputDir = Path.Combine(TempRoot.Root, "gendir");
        var assemblyPath = Path.Combine(TempRoot.Root, "assemblyDir", "assembly.dll");
 
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var project = AddEmptyProject(workspace.CurrentSolution)
            .WithCompilationOutputInfo(new CompilationOutputInfo(assemblyPath, generatedFilesOutputDir))
            .AddAnalyzerReference(analyzerReference);
 
        // Optionally fetch the compilation first, which validates that we handle both running the generator
        // when the file already exists, and when it is added incrementally.
        Compilation? originalCompilation = null;
 
        if (fetchCompilationBeforeAddingAdditionalFile)
        {
            originalCompilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
        }
 
        project = project.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
        var newCompilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
 
        Assert.NotSame(originalCompilation, newCompilation);
        var generatedTree = Assert.Single(newCompilation.SyntaxTrees);
        var generatorType = typeof(GenerateFileForEachAdditionalFileWithContentsCommented);
 
        Assert.Equal(Path.Combine(generatedFilesOutputDir, generatorType.Assembly.GetName().Name!, generatorType.FullName!, "Test.generated.cs"), generatedTree.FilePath);
 
        var generatedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync());
        Assert.Same(generatedTree, await generatedDocument.GetSyntaxTreeAsync());
    }
 
    [Theory, CombinatorialData, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1655835")]
    public async Task WithReferencesMethodCorrectlyUpdatesWithEqualReferences(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        // AnalyzerReferences may implement equality (AnalyezrFileReference does), and we want to make sure if we substitute out one
        // reference with another reference that's equal, we correctly update generators. We'll have the underlying generators
        // be different since two AnalyzerFileReferences that are value equal but different instances would have their own generators as well.
        const string SharedPath = "Z:\\Generator.dll";
        static ISourceGenerator CreateGenerator() => new SingleFileTestGenerator("// StaticContent", hintName: "generated");
 
        var analyzerReference1 = new TestGeneratorReferenceWithFilePathEquality(CreateGenerator(), SharedPath);
        var analyzerReference2 = new TestGeneratorReferenceWithFilePathEquality(CreateGenerator(), SharedPath);
 
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference1);
 
        Assert.Single((await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees);
 
        // Go from one analyzer reference to the other
        project = project.WithAnalyzerReferences([analyzerReference2]);
 
        Assert.Single((await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees);
 
        // Now remove and confirm that we don't have any files
        project = project.WithAnalyzerReferences([]);
 
        Assert.Empty((await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees);
    }
 
    private class TestGeneratorReferenceWithFilePathEquality : TestGeneratorReference, IEquatable<AnalyzerReference>
    {
        public TestGeneratorReferenceWithFilePathEquality(ISourceGenerator generator, string analyzerFilePath)
            : base(generator, analyzerFilePath)
        {
        }
 
        public override bool Equals(object? obj) => Equals(obj as AnalyzerReference);
        public override string FullPath => base.FullPath!; // This derived class always has this non-null
        public override int GetHashCode() => this.FullPath.GetHashCode();
 
        public bool Equals(AnalyzerReference? other)
        {
            return other is TestGeneratorReferenceWithFilePathEquality otherReference &&
                this.FullPath == otherReference.FullPath;
        }
    }
 
    [Theory, CombinatorialData]
    public async Task WithReferencesMethodCorrectlyAddsAndRemovesRunningGenerators(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        // We always have a single generator in this test, and we add or remove a second one. This is critical
        // to ensuring we correctly update our existing GeneratorDriver we may have from a prior run with the new
        // generators passed to WithAnalyzerReferences. If we only swap from zero generators to one generator,
        // we don't have a prior GeneratorDriver to update, since we don't make a GeneratorDriver if we have no generators.
        // Similarly, once we go from one back to zero, we end up getting rid of our GeneratorDriver entirely since
        // we have no need for it, as an optimization.
        var generatorReferenceToKeep = new TestGeneratorReference(new SingleFileTestGenerator("// StaticContent", hintName: "generatorReferenceToKeep"));
        var analyzerReferenceToAddAndRemove = new TestGeneratorReference(new SingleFileTestGenerator2("// More Static Content", hintName: "analyzerReferenceToAddAndRemove"));
 
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(generatorReferenceToKeep);
 
        Assert.Single((await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees);
 
        // Go from one generator to two.
        project = project.WithAnalyzerReferences([generatorReferenceToKeep, analyzerReferenceToAddAndRemove]);
 
        Assert.Equal(2, (await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees.Count());
 
        // And go back to one
        project = project.WithAnalyzerReferences([generatorReferenceToKeep]);
 
        Assert.Single((await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees);
    }
 
    // We only run this test on Release, as the compiler has asserts that trigger in Debug that the type names probably shouldn't be the same.
    [ConditionalTheory(typeof(IsRelease)), CombinatorialData]
    public async Task GeneratorAddedWithDifferentFilePathsProducesDistinctDocumentIds(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        // Produce two generator references with different paths, but the same generator by assembly/type. We will still give them separate
        // generator instances, because in the "real" analyzer reference case each analyzer reference produces it's own generator objects.
        var generatorReference1 = new TestGeneratorReference(new SingleFileTestGenerator("", hintName: "DuplicateFile"), analyzerFilePath: "Z:\\A.dll");
        var generatorReference2 = new TestGeneratorReference(new SingleFileTestGenerator("", hintName: "DuplicateFile"), analyzerFilePath: "Z:\\B.dll");
 
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReferences([generatorReference1, generatorReference2]);
 
        Assert.Equal(2, (await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees.Count());
 
        var generatedDocuments = (await project.GetSourceGeneratedDocumentsAsync()).ToList();
        Assert.Equal(2, generatedDocuments.Count);
 
        Assert.NotEqual(generatedDocuments[0].Id, generatedDocuments[1].Id);
    }
 
    [Fact]
    public async Task IncrementalSourceGeneratorInvokedCorrectNumberOfTimes()
    {
        using var workspace = CreateWorkspace([typeof(TestCSharpCompilationFactoryServiceWithIncrementalGeneratorTracking)]);
        var generator = new GenerateFileForEachAdditionalFileWithContentsCommented();
        var analyzerReference = new TestGeneratorReference(generator);
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project
            .AddAdditionalDocument("Test2.txt", "Hello, world!").Project;
 
        var compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
 
        var generatorDriver = project.Solution.CompilationState.GetTestAccessor().GetGeneratorDriver(project)!;
 
        var runResult = generatorDriver!.GetRunResult().Results[0];
 
        Assert.Equal(2, compilation.SyntaxTrees.Count());
        Assert.Equal(2, runResult.TrackedSteps[GenerateFileForEachAdditionalFileWithContentsCommented.StepName].Length);
        Assert.All(runResult.TrackedSteps[GenerateFileForEachAdditionalFileWithContentsCommented.StepName],
            step =>
            {
                Assert.Collection(step.Inputs,
                    source => Assert.Equal(IncrementalStepRunReason.New, source.Source.Outputs[source.OutputIndex].Reason));
                Assert.Collection(step.Outputs,
                    output => Assert.Equal(IncrementalStepRunReason.New, output.Reason));
            });
 
        // Change one of the additional documents, and rerun; we should only reprocess that one change, since this
        // is an incremental generator.
        project = project.AdditionalDocuments.First().WithAdditionalDocumentText(SourceText.From("Changed text!")).Project;
 
        compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
        generatorDriver = project.Solution.CompilationState.GetTestAccessor().GetGeneratorDriver(project)!;
        runResult = generatorDriver.GetRunResult().Results[0];
 
        Assert.Equal(2, compilation.SyntaxTrees.Count());
        Assert.Equal(2, runResult.TrackedSteps[GenerateFileForEachAdditionalFileWithContentsCommented.StepName].Length);
        Assert.Contains(runResult.TrackedSteps[GenerateFileForEachAdditionalFileWithContentsCommented.StepName],
            step =>
            {
                return step.Inputs.Length == 1
                && step.Inputs[0].Source.Outputs[step.Inputs[0].OutputIndex].Reason == IncrementalStepRunReason.Modified
                && step.Outputs is [{ Reason: IncrementalStepRunReason.Modified }];
            });
        Assert.Contains(runResult.TrackedSteps[GenerateFileForEachAdditionalFileWithContentsCommented.StepName],
            step =>
            {
                return step.Inputs.Length == 1
                && step.Inputs[0].Source.Outputs[step.Inputs[0].OutputIndex].Reason == IncrementalStepRunReason.Cached
                && step.Outputs is [{ Reason: IncrementalStepRunReason.Cached }];
            });
 
        // Change one of the source documents, and rerun; we should again only reprocess that one change.
        project = project.AddDocument("Source.cs", SourceText.From("")).Project;
 
        compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
        generatorDriver = project.Solution.CompilationState.GetTestAccessor().GetGeneratorDriver(project)!;
        runResult = generatorDriver.GetRunResult().Results[0];
 
        // We have one extra syntax tree now, but it did not require any invocations of the incremental generator.
        Assert.Equal(3, compilation.SyntaxTrees.Count());
        Assert.Equal(2, runResult.TrackedSteps[GenerateFileForEachAdditionalFileWithContentsCommented.StepName].Length);
        Assert.All(runResult.TrackedSteps[GenerateFileForEachAdditionalFileWithContentsCommented.StepName],
            step =>
            {
                Assert.Collection(step.Inputs,
                    source => Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason));
                Assert.Collection(step.Outputs,
                    output => Assert.Equal(IncrementalStepRunReason.Cached, output.Reason));
            });
    }
 
    [Theory, CombinatorialData]
    public async Task SourceGeneratorContentStillIncludedAfterSourceFileChange(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("Hello.cs", "// Source File").Project
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
        var documentId = project.DocumentIds.Single();
 
        await AssertCompilationContainsOneRegularAndOneGeneratedFile(project, documentId, "// Hello, world!");
 
        project = project.Solution.WithDocumentText(documentId, SourceText.From("// Changed Source File")).Projects.Single();
 
        await AssertCompilationContainsOneRegularAndOneGeneratedFile(project, documentId, "// Hello, world!");
 
        static async Task AssertCompilationContainsOneRegularAndOneGeneratedFile(Project project, DocumentId documentId, string expectedGeneratedContents)
        {
            var compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
 
            var regularDocumentSyntaxTree = await project.GetRequiredDocument(documentId).GetRequiredSyntaxTreeAsync(CancellationToken.None);
            Assert.Contains(regularDocumentSyntaxTree, compilation.SyntaxTrees);
 
            var generatedSyntaxTree = Assert.Single(compilation.SyntaxTrees.Where(t => t != regularDocumentSyntaxTree));
            Assert.IsType<SourceGeneratedDocument>(project.GetDocument(generatedSyntaxTree));
 
            Assert.Equal(expectedGeneratedContents, generatedSyntaxTree.GetText().ToString());
        }
    }
 
    // This will make a series of changes to additional files and assert that we correctly update generated output at various times.
    // By making this a theory with a bunch of booleans, it tests that we are correctly handling the situation where we queue up multiple changes
    // to the Compilation at once.
    [Theory, CombinatorialData]
    public async Task SourceGeneratorContentChangesAfterAdditionalFileChanges(
        bool assertRightAway,
        bool assertAfterAdd,
        bool assertAfterFirstChange,
        bool assertAfterSecondChange,
        TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference);
 
        if (assertRightAway)
            await AssertCompilationContainsGeneratedFilesAsync(project, expectedGeneratedContents: []);
 
        project = project.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
        var additionalDocumentId = project.AdditionalDocumentIds.Single();
 
        if (assertAfterAdd)
            await AssertCompilationContainsGeneratedFilesAsync(project, "// Hello, world!");
 
        project = project.Solution.WithAdditionalDocumentText(additionalDocumentId, SourceText.From("Hello, everyone!")).Projects.Single();
 
        if (assertAfterFirstChange)
            await AssertCompilationContainsGeneratedFilesAsync(project, "// Hello, everyone!");
 
        project = project.Solution.WithAdditionalDocumentText(additionalDocumentId, SourceText.From("Good evening, everyone!")).Projects.Single();
 
        if (assertAfterSecondChange)
            await AssertCompilationContainsGeneratedFilesAsync(project, "// Good evening, everyone!");
 
        project = project.RemoveAdditionalDocument(additionalDocumentId);
 
        await AssertCompilationContainsGeneratedFilesAsync(project, expectedGeneratedContents: []);
 
        static async Task AssertCompilationContainsGeneratedFilesAsync(Project project, params string[] expectedGeneratedContents)
        {
            var compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
 
            foreach (var tree in compilation.SyntaxTrees)
                Assert.IsType<SourceGeneratedDocument>(project.GetDocument(tree));
 
            var texts = compilation.SyntaxTrees.Select(t => t.GetText().ToString());
            AssertEx.SetEqual(expectedGeneratedContents, texts);
        }
    }
 
    [Theory, CombinatorialData]
    public async Task PartialCompilationsIncludeGeneratedFilesAfterFullGeneration(
        TestHost testHost, bool forceFreeze)
    {
        using var workspace = CreateWorkspaceWithPartialSemantics(testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("Hello.cs", "// Source File").Project
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
        var fullCompilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
 
        Assert.Equal(2, fullCompilation.SyntaxTrees.Count());
 
        var partialProject = project.Documents.Single().WithFrozenPartialSemantics(forceFreeze, CancellationToken.None).Project;
 
        // If we're forcing the freeze, we must get a different project instance.  If we're not, we'll get the same
        // project since the compilation was already available.
        if (forceFreeze)
            Assert.NotSame(partialProject, project);
        else
            Assert.Same(partialProject, project);
 
        var partialCompilation = await partialProject.GetRequiredCompilationAsync(CancellationToken.None);
 
        Assert.Same(fullCompilation, partialCompilation);
    }
 
    [Theory, CombinatorialData]
    public async Task DocumentIdOfGeneratedDocumentsIsStable(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var projectBeforeChange = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
        var generatedDocumentBeforeChange = Assert.Single(await projectBeforeChange.GetSourceGeneratedDocumentsAsync());
 
        var projectAfterChange =
            projectBeforeChange.Solution.WithAdditionalDocumentText(
                projectBeforeChange.AdditionalDocumentIds.Single(),
                SourceText.From("Hello, world!!!!")).Projects.Single();
 
        var generatedDocumentAfterChange = Assert.Single(await projectAfterChange.GetSourceGeneratedDocumentsAsync());
 
        Assert.NotSame(generatedDocumentBeforeChange, generatedDocumentAfterChange);
        Assert.Equal(generatedDocumentBeforeChange.Id, generatedDocumentAfterChange.Id);
    }
 
    [Theory, CombinatorialData]
    public async Task DocumentIdGuidInDifferentProjectsIsDifferent(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
 
        var solutionWithProjects = AddProjectWithReference(workspace.CurrentSolution, analyzerReference);
        solutionWithProjects = AddProjectWithReference(solutionWithProjects, analyzerReference);
 
        var projectIds = solutionWithProjects.ProjectIds.ToList();
 
        var generatedDocumentsInFirstProject = await solutionWithProjects.GetRequiredProject(projectIds[0]).GetSourceGeneratedDocumentsAsync();
        var generatedDocumentsInSecondProject = await solutionWithProjects.GetRequiredProject(projectIds[1]).GetSourceGeneratedDocumentsAsync();
 
        // A DocumentId consists of a GUID and then the ProjectId it's within. Even if these two documents have the same GUID,
        // they'll still be not equal because of the different ProjectIds. However, we'll also assert the GUIDs should be different as well,
        // because otherwise things can get confusing. If nothing else, the DocumentId debugger display string shows only the GUID, so you could
        // easily confuse them as being the same.
        Assert.NotEqual(generatedDocumentsInFirstProject.Single().Id.Id, generatedDocumentsInSecondProject.Single().Id.Id);
 
        static Solution AddProjectWithReference(Solution solution, TestGeneratorReference analyzerReference)
        {
            var project = AddEmptyProject(solution);
 
            project = project.AddAnalyzerReference(analyzerReference);
            project = project.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
            return project.Solution;
        }
    }
 
    [Theory, CombinatorialData]
    public async Task CompilationsInCompilationReferencesIncludeGeneratedSourceFiles(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var solution = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project.Solution;
 
        var projectIdWithGenerator = solution.ProjectIds.Single();
        var projectIdWithReference = ProjectId.CreateNewId();
 
        solution = solution.AddProject(projectIdWithReference, "WithReference", "WithReference", LanguageNames.CSharp)
                           .AddProjectReference(projectIdWithReference, new ProjectReference(projectIdWithGenerator));
 
        var compilationWithReference = await solution.GetRequiredProject(projectIdWithReference).GetRequiredCompilationAsync(CancellationToken.None);
 
        var compilationReference = Assert.IsAssignableFrom<CompilationReference>(Assert.Single(compilationWithReference.References));
 
        var compilationWithGenerator = await solution.GetRequiredProject(projectIdWithGenerator).GetRequiredCompilationAsync(CancellationToken.None);
 
        Assert.NotEmpty(compilationWithGenerator.SyntaxTrees);
        Assert.Same(compilationWithGenerator, compilationReference.Compilation);
    }
 
    [Theory, CombinatorialData]
    public async Task GetDocumentWithGeneratedTreeReturnsGeneratedDocument(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
        var syntaxTree = Assert.Single((await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees);
        var generatedDocument = Assert.IsType<SourceGeneratedDocument>(project.GetDocument(syntaxTree));
        Assert.Same(syntaxTree, await generatedDocument.GetSyntaxTreeAsync());
    }
 
    [Theory, CombinatorialData]
    public async Task GetDocumentWithGeneratedTreeForInProgressReturnsGeneratedDocument(TestHost testHost)
    {
        using var workspace = CreateWorkspaceWithPartialSemantics(testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
        // Ensure we've ran generators at least once
        await project.GetCompilationAsync();
 
        // Produce an in-progress snapshot
        project = project.Documents.Single(d => d.Name == "RegularDocument.cs").WithFrozenPartialSemantics(CancellationToken.None).Project;
 
        // The generated tree should still be there; even if the regular compilation fell away we've now cached the 
        // generated trees.
        var syntaxTree = Assert.Single((await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees, t => t.FilePath != "RegularDocument.cs");
        var generatedDocument = Assert.IsType<SourceGeneratedDocument>(project.GetDocument(syntaxTree));
        Assert.Same(syntaxTree, await generatedDocument.GetSyntaxTreeAsync());
    }
 
    [Theory, CombinatorialData]
    public async Task TreeReusedIfGeneratedFileDoesNotChangeBetweenRuns(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new SingleFileTestGenerator("// StaticContent"));
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
        var generatedTreeBeforeChange = await Assert.Single(await project.GetSourceGeneratedDocumentsAsync()).GetSyntaxTreeAsync();
 
        // Mutate the regular document to produce a new compilation
        project = project.Documents.Single().WithText(SourceText.From("// Change")).Project;
 
        var generatedTreeAfterChange = await Assert.Single(await project.GetSourceGeneratedDocumentsAsync()).GetSyntaxTreeAsync();
 
        Assert.Same(generatedTreeBeforeChange, generatedTreeAfterChange);
    }
 
    [Theory, CombinatorialData]
    public async Task TreeNotReusedIfParseOptionsChangeChangeBetweenRuns(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new SingleFileTestGenerator("// StaticContent"));
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project
            .AddAdditionalDocument("Test.txt", "Hello, world!").Project;
 
        var generatedTreeBeforeChange = await Assert.Single(await project.GetSourceGeneratedDocumentsAsync()).GetSyntaxTreeAsync();
 
        // Mutate the parse options to produce a new compilation
        Assert.NotEqual(DocumentationMode.Diagnose, project.ParseOptions!.DocumentationMode);
        project = project.WithParseOptions(project.ParseOptions.WithDocumentationMode(DocumentationMode.Diagnose));
 
        var generatedTreeAfterChange = await Assert.Single(await project.GetSourceGeneratedDocumentsAsync()).GetSyntaxTreeAsync();
 
        Assert.NotSame(generatedTreeBeforeChange, generatedTreeAfterChange);
        Assert.Equal(DocumentationMode.Diagnose, generatedTreeAfterChange!.Options.DocumentationMode);
    }
 
    [Theory, CombinatorialData]
    public async Task ChangeToDocumentThatDoesNotImpactGeneratedDocumentReusesDeclarationTree(bool generatorProducesTree, TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        // We'll use either a generator that produces a single tree, or no tree, to ensure we efficiently handle both cases
        ISourceGenerator generator = generatorProducesTree ? new SingleFileTestGenerator("// StaticContent")
                                                           : new CallbackGenerator(onInit: _ => { }, onExecute: _ => { });
 
        var analyzerReference = new TestGeneratorReference(generator);
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
 
        // Ensure we already have a compilation created
        _ = await project.GetCompilationAsync();
 
        project = await MakeChangesToDocument(project);
 
        var compilationAfterFirstChange = await project.GetRequiredCompilationAsync(CancellationToken.None);
 
        project = await MakeChangesToDocument(project);
 
        var compilationAfterSecondChange = await project.GetRequiredCompilationAsync(CancellationToken.None);
 
        // When we produced compilationAfterSecondChange, what we would ideally like is that compilation was produced by taking
        // compilationAfterFirstChange and simply updating the syntax tree that changed, since the generated documents didn't change.
        // That allows the compiler to reuse the same declaration tree for the generated file. This is hard to observe directly, but if we reflect
        // into the Compilation we can see if the declaration tree is untouched. We won't look at the original compilation, since
        // that original one was produced by adding the generated file as the final step, so it's cache won't be reusable, since the
        // compiler separates the "most recently changed tree" in the declaration table for efficiency.
 
        var cachedStateAfterFirstChange = GetDeclarationManagerCachedStateForUnchangingTrees(compilationAfterFirstChange);
        var cachedStateAfterSecondChange = GetDeclarationManagerCachedStateForUnchangingTrees(compilationAfterSecondChange);
 
        Assert.Same(cachedStateAfterFirstChange, cachedStateAfterSecondChange);
 
        static object GetDeclarationManagerCachedStateForUnchangingTrees(Compilation compilation)
        {
            var syntaxAndDeclarationsManager = compilation.GetFieldValue("_syntaxAndDeclarations");
            var state = syntaxAndDeclarationsManager.GetType().GetMethod("GetLazyState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!.Invoke(syntaxAndDeclarationsManager, null);
            var declarationTable = state.GetFieldValue("DeclarationTable");
            return declarationTable.GetFieldValue("_cache");
        }
 
        static async Task<Project> MakeChangesToDocument(Project project)
        {
            var existingText = await project.Documents.Single().GetTextAsync();
            var newText = existingText.WithChanges(new TextChange(new TextSpan(existingText.Length, length: 0), " With Change"));
            project = project.Documents.Single().WithText(newText).Project;
            return project;
        }
    }
 
    [Theory, CombinatorialData]
    public async Task CompilationNotCreatedByFetchingGeneratedFilesIfNoGeneratorsPresent(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var project = AddEmptyProject(workspace.CurrentSolution);
 
        Assert.Empty(await project.GetSourceGeneratedDocumentsAsync());
 
        // We shouldn't have any compilation since we didn't have to run anything
        Assert.False(project.TryGetCompilation(out _));
    }
 
    [Theory, CombinatorialData]
    public async Task OpenSourceGeneratedUpdatedToBufferContentsWhenCallingGetOpenDocumentInCurrentContextWithChanges(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new SingleFileTestGenerator("// StaticContent"));
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference);
 
        Assert.True(workspace.SetCurrentSolution(_ => project.Solution, WorkspaceChangeKind.SolutionChanged));
 
        var generatedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync());
        var differentOpenTextContainer = SourceText.From("// Open Text").Container;
 
        workspace.OnSourceGeneratedDocumentOpened(differentOpenTextContainer, generatedDocument);
 
        generatedDocument = Assert.IsType<SourceGeneratedDocument>(differentOpenTextContainer.CurrentText.GetOpenDocumentInCurrentContextWithChanges());
        Assert.Same(differentOpenTextContainer.CurrentText, await generatedDocument.GetTextAsync());
        Assert.NotSame(workspace.CurrentSolution, generatedDocument.Project.Solution);
 
        var generatedTree = await generatedDocument.GetSyntaxTreeAsync();
        var compilation = await generatedDocument.Project.GetRequiredCompilationAsync(CancellationToken.None);
        Assert.Contains(generatedTree, compilation.SyntaxTrees);
    }
 
    [Theory, CombinatorialData]
    public async Task OpenSourceGeneratedFileDoesNotCreateNewSnapshotIfContentsKnownToMatch(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new SingleFileTestGenerator("// StaticContent"));
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference);
 
        Assert.True(workspace.SetCurrentSolution(_ => project.Solution, WorkspaceChangeKind.SolutionChanged));
 
        var generatedDocument = Assert.Single(await workspace.CurrentSolution.Projects.Single().GetSourceGeneratedDocumentsAsync());
        var differentOpenTextContainer = SourceText.From("// StaticContent", Encoding.UTF8).Container;
 
        workspace.OnSourceGeneratedDocumentOpened(differentOpenTextContainer, generatedDocument);
 
        generatedDocument = Assert.IsType<SourceGeneratedDocument>(differentOpenTextContainer.CurrentText.GetOpenDocumentInCurrentContextWithChanges());
        Assert.Same(workspace.CurrentSolution, generatedDocument!.Project.Solution);
    }
 
    [Theory, CombinatorialData]
    public async Task OpenSourceGeneratedFileMatchesBufferContentsEvenIfGeneratedFileIsMissingIsRemoved(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
        var originalAdditionalFile = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddAdditionalDocument("Test.txt", SourceText.From(""));
 
        Assert.True(workspace.SetCurrentSolution(_ => originalAdditionalFile.Project.Solution, WorkspaceChangeKind.SolutionChanged));
 
        var generatedDocument = Assert.Single(await originalAdditionalFile.Project.GetSourceGeneratedDocumentsAsync());
        var differentOpenTextContainer = SourceText.From("// Open Text").Container;
 
        workspace.OnSourceGeneratedDocumentOpened(differentOpenTextContainer, generatedDocument);
        workspace.OnAdditionalDocumentRemoved(originalAdditionalFile.Id);
 
        // At this point there should be no generated documents, even though our file is still open
        Assert.Empty(await workspace.CurrentSolution.Projects.Single().GetSourceGeneratedDocumentsAsync());
 
        generatedDocument = Assert.IsType<SourceGeneratedDocument>(differentOpenTextContainer.CurrentText.GetOpenDocumentInCurrentContextWithChanges());
        Assert.Same(differentOpenTextContainer.CurrentText, await generatedDocument.GetTextAsync());
 
        var generatedTree = await generatedDocument.GetSyntaxTreeAsync();
        var compilation = await generatedDocument.Project.GetRequiredCompilationAsync(CancellationToken.None);
        Assert.Contains(generatedTree, compilation.SyntaxTrees);
    }
 
    [Theory, CombinatorialData]
    public async Task OpenSourceGeneratedDocumentUpdatedAndVisibleInProjectReference(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new SingleFileTestGenerator("// StaticContent"));
        var solution = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference).Solution;
        var projectIdWithGenerator = solution.ProjectIds.Single();
 
        solution = AddEmptyProject(solution).AddProjectReference(
            new ProjectReference(projectIdWithGenerator)).Solution;
 
        Assert.True(workspace.SetCurrentSolution(_ => solution, WorkspaceChangeKind.SolutionChanged));
 
        var generatedDocument = Assert.Single(await workspace.CurrentSolution.GetRequiredProject(projectIdWithGenerator).GetSourceGeneratedDocumentsAsync());
        var differentOpenTextContainer = SourceText.From("// Open Text").Container;
 
        workspace.OnSourceGeneratedDocumentOpened(differentOpenTextContainer, generatedDocument);
 
        generatedDocument = Assert.IsType<SourceGeneratedDocument>(differentOpenTextContainer.CurrentText.GetOpenDocumentInCurrentContextWithChanges());
        var generatedTree = await generatedDocument.GetSyntaxTreeAsync();
 
        // Fetch the compilation from the other project, it should have a compilation reference that
        // contains the generated tree
        var projectWithReference = generatedDocument.Project.Solution.Projects.Single(p => p.Id != projectIdWithGenerator);
        var compilationWithReference = await projectWithReference.GetRequiredCompilationAsync(CancellationToken.None);
        var compilationReference = Assert.Single(compilationWithReference.References.OfType<CompilationReference>());
 
        Assert.Contains(generatedTree, compilationReference.Compilation.SyntaxTrees);
    }
 
    [Theory, CombinatorialData]
    public async Task OpenSourceGeneratedDocumentsUpdateIsDocumentOpenAndCloseWorks(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
        var analyzerReference = new TestGeneratorReference(new SingleFileTestGenerator("// StaticContent"));
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference);
 
        Assert.True(workspace.SetCurrentSolution(_ => project.Solution, WorkspaceChangeKind.SolutionChanged));
 
        var generatedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync());
        var differentOpenTextContainer = SourceText.From("// Open Text").Container;
 
        workspace.OnSourceGeneratedDocumentOpened(differentOpenTextContainer, generatedDocument);
 
        Assert.True(workspace.IsDocumentOpen(generatedDocument.Identity.DocumentId));
 
        var document = await workspace.CurrentSolution.GetSourceGeneratedDocumentAsync(generatedDocument.Identity.DocumentId, CancellationToken.None);
        Contract.ThrowIfNull(document);
        workspace.OnSourceGeneratedDocumentClosed(document);
 
        Assert.False(workspace.IsDocumentOpen(generatedDocument.Identity.DocumentId));
        Assert.Null(differentOpenTextContainer.CurrentText.GetOpenDocumentInCurrentContextWithChanges());
    }
 
    [Theory, CombinatorialData]
    public async Task FreezingSolutionEnsuresGeneratorsDoNotRun(bool forkBeforeFreeze, TestHost testHost)
    {
        var generatorRan = false;
        var generator = new CallbackGenerator(onInit: _ => { }, onExecute: _ => { generatorRan = true; });
 
        using var workspace = CreateWorkspaceWithPartialSemantics(testHost);
        var analyzerReference = new TestGeneratorReference(generator);
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
 
        Assert.True(workspace.SetCurrentSolution(_ => project.Solution, WorkspaceChangeKind.SolutionChanged));
 
        var documentToFreeze = workspace.CurrentSolution.Projects.Single().Documents.Single();
 
        // The generator shouldn't have ran before any of this since we didn't do anything that would ask for a compilation
        Assert.False(generatorRan);
 
        if (forkBeforeFreeze)
        {
            // Forking before freezing means we'll have to do extra work to produce the final compilation, but we should still
            // not be running generators
            documentToFreeze = documentToFreeze.WithText(SourceText.From("// Changed Source File"));
        }
 
        var frozenDocument = documentToFreeze.WithFrozenPartialSemantics(CancellationToken.None);
        Assert.NotSame(frozenDocument, documentToFreeze);
        await frozenDocument.GetSemanticModelAsync(CancellationToken.None);
 
        Assert.False(generatorRan);
    }
 
    [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/56702")]
    public async Task ForkAfterForceFreezeNoLongerRunsGenerators(TestHost testHost)
    {
        using var workspace = CreateWorkspaceWithPartialSemantics(testHost);
        var generatorRan = false;
        var analyzerReference = new TestGeneratorReference(new CallbackGenerator(_ => { }, onExecute: _ => { generatorRan = true; }, source: "// Hello World!"));
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
 
        // Ensure generators are ran
        var objectReference = await project.GetCompilationAsync();
 
        Assert.True(generatorRan);
        generatorRan = false;
 
        var document = project.Documents.Single().WithFrozenPartialSemantics(forceFreeze: true, CancellationToken.None);
 
        // And fork with new contents; we'll ensure the contents of this tree are different, but the generator will not
        // run since we explicitly force froze.
        document = document.WithText(SourceText.From("// Something else"));
 
        var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None);
        Assert.Equal(2, compilation.SyntaxTrees.Count());
        Assert.False(generatorRan);
 
        Assert.Equal("// Something else", (await document.GetRequiredSyntaxRootAsync(CancellationToken.None)).ToFullString());
    }
 
    [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/56702")]
    public async Task ForkAfterFreezeOfCompletedDocumentStillRunsGenerators(TestHost testHost)
    {
        using var workspace = CreateWorkspaceWithPartialSemantics(testHost);
        var generatorRan = false;
        var analyzerReference = new TestGeneratorReference(new CallbackGenerator(_ => { }, onExecute: _ => { generatorRan = true; }, source: "// Hello World!"));
        var project = AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
 
        // Ensure generators are ran
        var objectReference = await project.GetCompilationAsync();
 
        Assert.True(generatorRan);
        generatorRan = false;
 
        var document = project.Documents.Single().WithFrozenPartialSemantics(CancellationToken.None);
 
        // And fork with new contents; we'll ensure the contents of this tree are different, but the generator will run
        // since we didn't force freeze, and we got the frozen document after its compilation was already computed.
        document = document.WithText(SourceText.From("// Something else"));
 
        var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None);
        Assert.Equal(2, compilation.SyntaxTrees.Count());
        Assert.True(generatorRan);
 
        Assert.Equal("// Something else", (await document.GetRequiredSyntaxRootAsync(CancellationToken.None)).ToFullString());
    }
 
    [Theory, CombinatorialData]
    public async Task LinkedDocumentOfFrozenShouldNotRunSourceGenerator(TestHost testHost)
    {
        using var workspace = CreateWorkspaceWithPartialSemantics(testHost);
        var generatorRan = false;
        var analyzerReference = new TestGeneratorReference(new CallbackGenerator(_ => { }, onExecute: _ => { generatorRan = true; }, source: "// Hello World!"));
 
        var originalDocument1 = AddEmptyProject(workspace.CurrentSolution, name: "Project1")
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs");
 
        // this is a linked document of document1 above
        var originalDocument2 = AddEmptyProject(originalDocument1.Project.Solution, name: "Project2")
            .AddAnalyzerReference(analyzerReference)
            .AddDocument(originalDocument1.Name, await originalDocument1.GetTextAsync().ConfigureAwait(false), filePath: originalDocument1.FilePath);
 
        var frozenSolution = originalDocument2.WithFrozenPartialSemantics(CancellationToken.None).Project.Solution;
        var documentIdsToTest = new[] { originalDocument1.Id, originalDocument2.Id };
 
        foreach (var documentIdToTest in documentIdsToTest)
        {
            var document = frozenSolution.GetRequiredDocument(documentIdToTest);
            Assert.Single(document.GetLinkedDocumentIds());
 
            Assert.Equal(document.GetLinkedDocumentIds().Single(), documentIdsToTest.Except([documentIdToTest]).Single());
            document = document.WithText(SourceText.From("// Something else"));
 
            var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None);
            Assert.Single(compilation.SyntaxTrees);
            Assert.False(generatorRan);
        }
    }
 
    [Theory, CombinatorialData]
    public async Task DynamicFilesNotPassedToSourceGenerators(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        bool? noTreesPassed = null;
 
        var analyzerReference = new TestGeneratorReference(
            new CallbackGenerator(
                onInit: _ => { },
                onExecute: context => noTreesPassed = context.Compilation.SyntaxTrees.Any()));
 
        var project = AddEmptyProject(workspace.CurrentSolution);
        var documentInfo = DocumentInfo.Create(
            DocumentId.CreateNewId(project.Id),
            name: "Test.cs",
            isGenerated: true).WithDesignTimeOnly(true);
 
        project = project.Solution.AddDocument(documentInfo).Projects.Single()
            .AddAnalyzerReference(analyzerReference);
 
        _ = await project.GetCompilationAsync();
 
        // We should have ran the generator, and it should not have had any trees
        Assert.True(noTreesPassed.HasValue);
        Assert.False(noTreesPassed!.Value);
    }
 
    [Theory, CombinatorialData]
    public async Task FreezingSourceGeneratedDocumentsWorks(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        var analyzerReference = new TestGeneratorReference(
            new SingleFileTestGenerator("// Hello, World"));
 
        var project = AddEmptyProject(workspace.CurrentSolution).AddAnalyzerReference(analyzerReference);
 
        var sourceGeneratedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync());
        var sourceGeneratedDocumentIdentity = sourceGeneratedDocument.Identity;
 
        // Do some assertions with freezing that document
        await AssertFrozen(project, sourceGeneratedDocumentIdentity);
 
        // Now remove the generator, and ensure we can freeze it even if it's not there. This scenario exists for IDEs where 
        // a text buffer might still be wired up to the workspace and we're invoking a feature on it. The generated document might have gone
        // away, but we don't know that synchronously.
        project = project.RemoveAnalyzerReference(analyzerReference);
        await AssertFrozen(project, sourceGeneratedDocumentIdentity);
 
        static async Task AssertFrozen(Project project, SourceGeneratedDocumentIdentity identity)
        {
            var frozenWithSingleDocument = project.Solution.WithFrozenSourceGeneratedDocument(
                identity, DateTime.Now, SourceText.From("// Frozen Document"));
            Assert.Equal("// Frozen Document", (await frozenWithSingleDocument.GetTextAsync()).ToString());
            var syntaxTrees = (await frozenWithSingleDocument.Project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees;
            var frozenTree = Assert.Single(syntaxTrees);
            Assert.Equal("// Frozen Document", frozenTree.ToString());
        }
    }
 
    [Theory, CombinatorialData]
    public async Task FreezingSourceGeneratedDocumentsInTwoProjectsWorks(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        var analyzerReference = new TestGeneratorReference(
            new SingleFileTestGenerator("// Hello, World"));
 
        var solution = AddEmptyProject(workspace.CurrentSolution).AddAnalyzerReference(analyzerReference).Solution;
        var projectId1 = solution.ProjectIds.Single();
        var project2 = AddEmptyProject(solution, name: "TestProject2").AddAnalyzerReference(analyzerReference);
        solution = project2.Solution;
        var projectId2 = project2.Id;
 
        var sourceGeneratedDocument1 = Assert.Single(await solution.GetRequiredProject(projectId1).GetSourceGeneratedDocumentsAsync());
        var sourceGeneratedDocument2 = Assert.Single(await solution.GetRequiredProject(projectId2).GetSourceGeneratedDocumentsAsync());
 
        // And now freeze both of them at once
        var solutionWithFrozenDocuments = solution.WithFrozenSourceGeneratedDocuments(
            [(sourceGeneratedDocument1.Identity, DateTime.Now, SourceText.From("// Frozen 1")), (sourceGeneratedDocument2.Identity, DateTime.Now, SourceText.From("// Frozen 2"))]);
 
        Assert.Equal("// Frozen 1", (await (await solutionWithFrozenDocuments.GetRequiredProject(projectId1).GetSourceGeneratedDocumentsAsync()).Single().GetTextAsync()).ToString());
        Assert.Equal("// Frozen 2", (await (await solutionWithFrozenDocuments.GetRequiredProject(projectId2).GetSourceGeneratedDocumentsAsync()).Single().GetTextAsync()).ToString());
    }
 
    [Theory, CombinatorialData]
    public async Task FreezingWithSameContentDoesNotFork(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        var analyzerReference = new TestGeneratorReference(
            new SingleFileTestGenerator("// Hello, World"));
 
        var project = AddEmptyProject(workspace.CurrentSolution).AddAnalyzerReference(analyzerReference);
 
        var sourceGeneratedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync());
        var sourceGeneratedDocumentIdentity = sourceGeneratedDocument.Identity;
 
        var frozenSolution = project.Solution.WithFrozenSourceGeneratedDocument(
            sourceGeneratedDocumentIdentity, sourceGeneratedDocument.GenerationDateTime, SourceText.From("// Hello, World"));
        Assert.Same(project.Solution, frozenSolution.Project.Solution);
    }
 
    [Theory, CombinatorialData]
    public async Task TestChangingGeneratorChangesChecksum(TestHost testHost)
    {
        using var workspace = CreateWorkspace(testHost: testHost);
 
        var analyzerReference1 = new TestGeneratorReference(
            new SingleFileTestGenerator("// Hello, World 1"));
        var analyzerReference2 = new TestGeneratorReference(
            new SingleFileTestGenerator("// Hello, World 2"));
 
        var project0 = AddEmptyProject(workspace.CurrentSolution);
        var checksum0 = await project0.Solution.SolutionState.GetChecksumAsync(CancellationToken.None);
 
        var project1 = project0.AddAnalyzerReference(analyzerReference1);
        var checksum1 = await project1.Solution.SolutionState.GetChecksumAsync(CancellationToken.None);
 
        Assert.NotEqual(project0, project1);
        Assert.NotEqual(checksum0, checksum1);
 
        var project2 = project1.RemoveAnalyzerReference(analyzerReference1);
        var checksum2 = await project2.Solution.SolutionState.GetChecksumAsync(CancellationToken.None);
 
        Assert.NotEqual(project0, project2);
        Assert.NotEqual(project1, project2);
 
        // Should still have the same checksum that we started with, even though we have different project instances.
        Assert.Equal(checksum0, checksum2);
        Assert.NotEqual(checksum1, checksum2);
 
        var project3 = project2.AddAnalyzerReference(analyzerReference2);
        var checksum3 = await project3.Solution.SolutionState.GetChecksumAsync(CancellationToken.None);
 
        Assert.NotEqual(project0, project3);
        Assert.NotEqual(project1, project3);
        Assert.NotEqual(project2, project3);
        Assert.NotEqual(checksum0, checksum3);
        Assert.NotEqual(checksum1, checksum3);
        Assert.NotEqual(checksum2, checksum3);
    }
 
#if NET

    private sealed class DoNotLoadAssemblyLoader : IAnalyzerAssemblyLoader
    {
        public static readonly IAnalyzerAssemblyLoader Instance = new DoNotLoadAssemblyLoader();
 
        public void AddDependencyLocation(string fullPath)
        {
        }
 
        public Assembly LoadFromPath(string fullPath)
            => throw new InvalidOperationException("These tests should not be loading analyzer assemblies in those host workspace, only in the remote one.");
    }
 
    [PartNotDiscoverable]
    [ExportWorkspaceService(typeof(IWorkspaceConfigurationService), ServiceLayer.Test), System.Composition.Shared]
    [method: ImportingConstructor]
    [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    private sealed class TestWorkspaceConfigurationService(IGlobalOptionService globalOptionService) : IWorkspaceConfigurationService
    {
        public WorkspaceConfigurationOptions Options => globalOptionService.GetWorkspaceConfigurationOptions();
    }
 
    [Theory, CombinatorialData]
    internal async Task UpdatingAnalyzerReferenceReloadsGenerators(
        SourceGeneratorExecutionPreference executionPreference)
    {
        // We have two versions of the same source generator attached to this project as a resource.  Each creates a
        // 'HelloWorld' class, just with a different string it emits inside.
        const string AnalyzerResourceV1 = @"Microsoft.CodeAnalysis.UnitTests.Resources.Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v1";
        const string AnalyzerResourceV2 = @"Microsoft.CodeAnalysis.UnitTests.Resources.Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v2";
 
        using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)], TestHost.OutOfProcess);
 
        // Ensure the local and remote sides agree on how we're executing source generators.
        var mefServices = (VisualStudioMefHostServices)workspace.Services.HostServices;
        var globalOptionService = mefServices.GetExportedValue<IGlobalOptionService>();
        globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, executionPreference);
 
        using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false);
 
        var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>();
 
        var remoteProcessId = await client.TryInvokeAsync<IRemoteProcessTelemetryService, int>(
            (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options with { SourceGeneratorExecution = executionPreference }, cancellationToken),
            CancellationToken.None).ConfigureAwait(false);
 
        var solution = workspace.CurrentSolution;
 
        var project1 = solution.AddProject("P1", "P1", LanguageNames.CSharp);
 
        using var tempRoot = new TempRoot();
        var tempDirectory = tempRoot.CreateDirectory();
 
        var analyzerPath = Path.Combine(tempDirectory.Path, "Microsoft.CodeAnalysis.TestAnalyzerReference.dll");
 
        var analyzerAssemblyLoaderProvider = workspace.Services.GetRequiredService<IAnalyzerAssemblyLoaderProvider>();
 
        // Add and test the v1 generator first.
        {
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(AnalyzerResourceV1))
            using (var destination = File.OpenWrite(analyzerPath))
            {
                stream!.CopyTo(destination);
            }
 
            // Pass in an always throwing assembly loader so we can be sure that no loading happens on the host side.
            project1 = project1.WithAnalyzerReferences([new AnalyzerFileReference(analyzerPath, DoNotLoadAssemblyLoader.Instance)]);
 
            var generatedDocuments = await project1.GetSourceGeneratedDocumentsAsync();
            var helloWorldDoc = generatedDocuments.Single(d => d.Name == "HelloWorld.cs");
 
            var contents = await helloWorldDoc.GetTextAsync();
            Assert.True(contents.ToString().Contains("Hello, World 1!"));
        }
 
        // Now, overwrite the analyzer reference with a new version that generates different contents
        {
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(AnalyzerResourceV2))
            using (var destination = File.OpenWrite(analyzerPath))
            {
                stream!.CopyTo(destination);
            }
 
            // Make a new analyzer reference to that location (note: with the same throwing assembly loader).  on the
            // host side, this will simply instantiate a new reference.  But this will cause all the machinery to run
            // syncing this new reference to the oop side, which will load the analyzer reference in a dedicated ALC.
            project1 = project1.WithAnalyzerReferences([new AnalyzerFileReference(analyzerPath, DoNotLoadAssemblyLoader.Instance)]);
 
            // In balanced mode, emulate the project system notifying about the updated reference on disk, which will
            // cause us to update source generators versions.
            if (executionPreference is SourceGeneratorExecutionPreference.Balanced)
            {
                Assert.True(workspace.TryApplyChanges(project1.Solution));
                workspace.EnqueueUpdateSourceGeneratorVersion(project1.Id, forceRegeneration: true);
 
                var waiter = (IAsynchronousOperationWaiter)mefServices.GetExportedValue<IAsynchronousOperationListenerProvider>().GetListener(FeatureAttribute.SourceGenerators);
                await waiter.ExpeditedWaitAsync();
 
                project1 = workspace.CurrentSolution.GetRequiredProject(project1.Id);
            }
 
            var generatedDocuments = await project1.GetSourceGeneratedDocumentsAsync();
            var helloWorldDoc = generatedDocuments.Single(d => d.Name == "HelloWorld.cs");
 
            // Note that the contents are now different than what we saw before.  This is with an analyzer at the same path,
            // with the same assembly name and type name for the generator.  Because there is a dedicated ALC, this reloads
            // fine.
            var contents = await helloWorldDoc.GetTextAsync();
            Assert.True(contents.ToString().Contains("Hello, World 2!"));
        }
    }
 
#endif
}