File: Workspace\Workspace_SourceGeneration.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Host;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
public partial class Workspace
{
    /// <summary>
    /// Used for batching up a lot of events and only combining them into a single request to update generators.  The
    /// <see cref="ProjectId"/> represents the projects that have changed, and which need their source-generators
    /// re-run.  <see langword="null"/> in the list indicates the entire solution has changed and all generators need to
    /// be rerun.  The <see cref="bool"/> represents if source generators should be fully rerun for the requested
    /// project or solution.  If <see langword="false"/>, the existing generator driver will be used, which may result
    /// in no actual changes to emitted source (as the driver may decide no inputs changed, and thus all outputs should
    /// be reused).  If <see langword="true"/>, the existing driver will be dropped, forcing all generation to be redone.
    /// </summary>
    private readonly AsyncBatchingWorkQueue<(ProjectId? projectId, bool forceRegeneration)> _updateSourceGeneratorsQueue;
 
    private readonly CancellationTokenSource _updateSourceGeneratorsQueueTokenSource = new();
 
    internal void EnqueueUpdateSourceGeneratorVersion(ProjectId? projectId, bool forceRegeneration)
        => _updateSourceGeneratorsQueue.AddWork((projectId, forceRegeneration));
 
    private async ValueTask ProcessUpdateSourceGeneratorRequestAsync(
        ImmutableSegmentedList<(ProjectId? projectId, bool forceRegeneration)> projectIds, CancellationToken cancellationToken)
    {
        var configuration = this.Services.GetRequiredService<IWorkspaceConfigurationService>().Options;
        if (configuration.SourceGeneratorExecution is SourceGeneratorExecutionPreference.Automatic)
        {
            // If we're in automatic mode, we don't need to do anything *unless* the host has asked us to
            // force-regenerate something.  In that case we're literally going to drop our generator drivers and
            // regenerate the code, so we can't depend on automatic running generators normally.
            if (!projectIds.Any(t => t.forceRegeneration))
                return;
        }
 
        await this.SetCurrentSolutionAsync(
            useAsync: true,
            oldSolution =>
            {
                var updates = GetUpdatedSourceGeneratorVersions(oldSolution, projectIds);
                return oldSolution.UpdateSpecificSourceGeneratorExecutionVersions(updates);
            },
            static (_, _) => (WorkspaceChangeKind.SolutionChanged, projectId: null, documentId: null),
            onBeforeUpdate: null,
            onAfterUpdate: null,
            cancellationToken).ConfigureAwait(false);
 
        return;
 
        // <summary>
        // Given the current state of a <paramref name="solution"/>, produced an updated version of the source generator
        // execution map based on the changes in <paramref name="projectIds"/>.  Each item in <paramref
        // name="projectIds"/> signifies a particular project (if <c>projectId</c> is non-null) or the solution as a
        // whole (if it is null). The <c>forceRegeneration</c> signifies if generators should be rerun even if the
        // contents of the solution are the same.  If a project is specified in <paramref name="projectIds"/> then both
        // it and all dependent projects of it will have their source generator versions updated.  If the solution is
        // specified, then all projects will have their versions updated.
        // </summary>
        static SourceGeneratorExecutionVersionMap GetUpdatedSourceGeneratorVersions(
            Solution solution, ImmutableSegmentedList<(ProjectId? projectId, bool forceRegeneration)> projectIds)
        {
            // For all the projects explicitly requested, update their source generator version.  Do this for all
            // projects that transitively depend on that project, so that their generators will run as well when next
            // asked.
            var dependencyGraph = solution.GetProjectDependencyGraph();
            var result = ImmutableSortedDictionary.CreateBuilder<ProjectId, SourceGeneratorExecutionVersion>();
 
            // Determine if we want a major solution change, forcing regeneration of all projects.
            var solutionMajor = projectIds.Any(t => t.projectId is null && t.forceRegeneration);
 
            // If it's not a major solution change, then go update the versions for all projects requested.
            if (!solutionMajor)
            {
                // Do a pass where we update minor versions if requested.
                PopulateSourceGeneratorExecutionVersions(major: false);
 
                // Then update major versions.  We do this after the minor-version pass so that major version updates
                // overwrite minor-version updates.
                PopulateSourceGeneratorExecutionVersions(major: true);
            }
 
            // Now, if we've been asked to do an entire solution update, get any projects we didn't already mark, and
            // update their execution version as well.
            if (projectIds.Any(t => t.projectId is null))
            {
                foreach (var projectId in solution.ProjectIds)
                {
                    if (!result.ContainsKey(projectId))
                    {
                        result.Add(
                            projectId,
                            Increment(solution.GetSourceGeneratorExecutionVersion(projectId), solutionMajor));
                    }
                }
            }
 
            return new(result.ToImmutable());
 
            void PopulateSourceGeneratorExecutionVersions(bool major)
            {
                foreach (var (projectId, forceRegeneration) in projectIds)
                {
                    if (projectId is null)
                        continue;
 
                    if (forceRegeneration != major)
                        continue;
 
                    // We may have been asked to rerun generators for a project that is no longer around.  So make sure
                    // we still have this project.
                    var requestedProject = solution.GetProject(projectId);
                    if (requestedProject != null)
                    {
                        result[projectId] = Increment(solution.GetSourceGeneratorExecutionVersion(projectId), major);
 
                        foreach (var transitiveProjectId in dependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectId))
                            result[transitiveProjectId] = Increment(solution.GetSourceGeneratorExecutionVersion(transitiveProjectId), major);
                    }
                }
            }
 
            static SourceGeneratorExecutionVersion Increment(SourceGeneratorExecutionVersion version, bool major)
                => major ? version.IncrementMajorVersion() : version.IncrementMinorVersion();
        }
    }
}