File: SolutionTests\SolutionWithSourceGeneratorTests.cs
Web Access
Project: ..\..\..\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.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
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 class SolutionWithSourceGeneratorTests : TestBase
    {
        [Theory]
        [CombinatorialData]
        public async Task SourceGeneratorBasedOnAdditionalFileGeneratesSyntaxTrees(
            bool fetchCompilationBeforeAddingAdditionalFile)
        {
            // This test is just the sanity test to make sure generators work at all. There's not a special scenario being
            // tested.
 
            using var workspace = CreateWorkspace();
            var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
            var project = AddEmptyProject(workspace.CurrentSolution)
                .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($"{generatorType.Assembly.GetName().Name}\\{generatorType.FullName}\\Test.generated.cs", generatedTree.FilePath);
 
            var generatedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync());
            Assert.Same(generatedTree, await generatedDocument.GetSyntaxTreeAsync());
        }
 
        [Fact]
        [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1655835")]
        public async Task WithReferencesMethodCorrectlyUpdatesWithEqualReferences()
        {
            using var workspace = CreateWorkspace();
 
            // 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";
            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(new[] { analyzerReference2 });
 
            Assert.Single((await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees);
 
            // Now remove and confirm that we don't have any files
            project = project.WithAnalyzerReferences(SpecializedCollections.EmptyEnumerable<AnalyzerReference>());
 
            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;
            }
        }
 
        [Fact]
        public async Task WithReferencesMethodCorrectlyAddsAndRemovesRunningGenerators()
        {
            using var workspace = CreateWorkspace();
 
            // 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(new[] { generatorReferenceToKeep, analyzerReferenceToAddAndRemove });
 
            Assert.Equal(2, (await project.GetRequiredCompilationAsync(CancellationToken.None)).SyntaxTrees.Count());
 
            // And go back to one
            project = project.WithAnalyzerReferences(new[] { 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.
        [ConditionalFact(typeof(IsRelease))]
        public async Task GeneratorAddedWithDifferentFilePathsProducesDistinctDocumentIds()
        {
            using var workspace = CreateWorkspace();
 
            // 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(new[] { 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(new[] { 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.State.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.State.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.State.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));
                });
        }
 
        [Fact]
        public async Task SourceGeneratorContentStillIncludedAfterSourceFileChange()
        {
            using var workspace = CreateWorkspace();
            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)
        {
            using var workspace = CreateWorkspace();
            var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
            var project = AddEmptyProject(workspace.CurrentSolution)
                .AddAnalyzerReference(analyzerReference);
 
            if (assertRightAway)
                await AssertCompilationContainsGeneratedFilesAsync(project, expectedGeneratedContents: new string[] { });
 
            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: new string[] { });
 
            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);
            }
        }
 
        [Fact]
        public async Task PartialCompilationsIncludeGeneratedFilesAfterFullGeneration()
        {
            using var workspace = CreateWorkspaceWithPartialSemantics();
            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(CancellationToken.None).Project;
            Assert.NotSame(partialProject, project);
            var partialCompilation = await partialProject.GetRequiredCompilationAsync(CancellationToken.None);
 
            Assert.Same(fullCompilation, partialCompilation);
        }
 
        [Fact]
        public async Task DocumentIdOfGeneratedDocumentsIsStable()
        {
            using var workspace = CreateWorkspace();
            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);
        }
 
        [Fact]
        public async Task DocumentIdGuidInDifferentProjectsIsDifferent()
        {
            using var workspace = CreateWorkspace();
            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;
            }
        }
 
        [Fact]
        public async Task CompilationsInCompilationReferencesIncludeGeneratedSourceFiles()
        {
            using var workspace = CreateWorkspace();
            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);
        }
 
        [Fact]
        public async Task GetDocumentWithGeneratedTreeReturnsGeneratedDocument()
        {
            using var workspace = CreateWorkspace();
            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());
        }
 
        [Fact]
        public async Task GetDocumentWithGeneratedTreeForInProgressReturnsGeneratedDocument()
        {
            using var workspace = CreateWorkspaceWithPartialSemantics();
            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());
        }
 
        [Fact]
        public async Task TreeReusedIfGeneratedFileDoesNotChangeBetweenRuns()
        {
            using var workspace = CreateWorkspace();
            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);
        }
 
        [Fact]
        public async Task TreeNotReusedIfParseOptionsChangeChangeBetweenRuns()
        {
            using var workspace = CreateWorkspace();
            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)
        {
            using var workspace = CreateWorkspace();
 
            // 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;
            }
        }
 
        [Fact]
        public async Task CompilationNotCreatedByFetchingGeneratedFilesIfNoGeneratorsPresent()
        {
            using var workspace = CreateWorkspace();
            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 _));
        }
 
        [Fact]
        public async Task OpenSourceGeneratedUpdatedToBufferContentsWhenCallingGetOpenDocumentInCurrentContextWithChanges()
        {
            using var workspace = CreateWorkspace();
            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);
        }
 
        [Fact]
        public async Task OpenSourceGeneratedFileDoesNotCreateNewSnapshotIfContentsKnownToMatch()
        {
            using var workspace = CreateWorkspace();
            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);
        }
 
        [Fact]
        public async Task OpenSourceGeneratedFileMatchesBufferContentsEvenIfGeneratedFileIsMissingIsRemoved()
        {
            using var workspace = CreateWorkspace();
            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);
        }
 
        [Fact]
        public async Task OpenSourceGeneratedDocumentUpdatedAndVisibleInProjectReference()
        {
            using var workspace = CreateWorkspace();
            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);
        }
 
        [Fact]
        public async Task OpenSourceGeneratedDocumentsUpdateIsDocumentOpenAndCloseWorks()
        {
            using var workspace = CreateWorkspace();
            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)
        {
            var generatorRan = false;
            var generator = new CallbackGenerator(onInit: _ => { }, onExecute: _ => { generatorRan = true; });
 
            using var workspace = CreateWorkspaceWithPartialSemantics();
            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);
        }
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/56702")]
        public async Task ForkAfterFreezeNoLongerRunsGenerators()
        {
            using var workspace = CreateWorkspaceWithPartialSemantics();
            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 still not be ran
            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());
        }
 
        [Fact]
        public async Task LinkedDocumentOfFrozenShouldNotRunSourceGenerator()
        {
            using var workspace = CreateWorkspaceWithPartialSemantics();
            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.Equal(document.GetLinkedDocumentIds().Single(), documentIdsToTest.Except(new[] { documentIdToTest }).Single());
                document = document.WithText(SourceText.From("// Something else"));
 
                var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None);
                Assert.Single(compilation.SyntaxTrees);
                Assert.False(generatorRan);
            }
        }
 
        [Fact]
        public async Task DynamicFilesNotPassedToSourceGenerators()
        {
            using var workspace = CreateWorkspace();
 
            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);
        }
    }
}