File: Workspace\Solution\StateChecksums.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;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Serialization;
 
internal sealed class SolutionCompilationStateChecksums
{
    public SolutionCompilationStateChecksums(
        Checksum solutionState,
        Checksum sourceGeneratorExecutionVersionMap,
        // These arrays are all the same length if present, and reference the same documents in the same order.
        DocumentChecksumsAndIds? frozenSourceGeneratedDocuments,
        ChecksumCollection? frozenSourceGeneratedDocumentIdentities,
        ImmutableArray<DateTime> frozenSourceGeneratedDocumentGenerationDateTimes)
    {
        // For the frozen source generated document info, we expect two either have both checksum collections or neither, and they
        // should both be the same length as there is a 1:1 correspondence between them.
        Contract.ThrowIfFalse(frozenSourceGeneratedDocumentIdentities.HasValue == frozenSourceGeneratedDocuments.HasValue);
        Contract.ThrowIfFalse(frozenSourceGeneratedDocumentIdentities?.Count == frozenSourceGeneratedDocuments?.Length);
 
        SolutionState = solutionState;
        SourceGeneratorExecutionVersionMap = sourceGeneratorExecutionVersionMap;
        FrozenSourceGeneratedDocuments = frozenSourceGeneratedDocuments;
        FrozenSourceGeneratedDocumentIdentities = frozenSourceGeneratedDocumentIdentities;
        FrozenSourceGeneratedDocumentGenerationDateTimes = frozenSourceGeneratedDocumentGenerationDateTimes;
 
        // note: intentionally not mixing in FrozenSourceGeneratedDocumentGenerationDateTimes as that is not part of the
        // identity contract of this type.
        Checksum = Checksum.Create(
            SolutionState,
            SourceGeneratorExecutionVersionMap,
            FrozenSourceGeneratedDocumentIdentities?.Checksum ?? Checksum.Null,
            frozenSourceGeneratedDocuments?.Checksum ?? Checksum.Null);
    }
 
    public Checksum Checksum { get; }
    public Checksum SolutionState { get; }
    public Checksum SourceGeneratorExecutionVersionMap { get; }
 
    /// <summary>
    /// Checksums of the SourceTexts of the frozen documents directly.  Not checksums of their DocumentStates.
    /// </summary>
    public DocumentChecksumsAndIds? FrozenSourceGeneratedDocuments { get; }
    public ChecksumCollection? FrozenSourceGeneratedDocumentIdentities { get; }
 
    // note: intentionally not part of the identity contract of this type.
    public ImmutableArray<DateTime> FrozenSourceGeneratedDocumentGenerationDateTimes { get; }
 
    public void AddAllTo(HashSet<Checksum> checksums)
    {
        checksums.AddIfNotNullChecksum(this.Checksum);
        checksums.AddIfNotNullChecksum(this.SolutionState);
        checksums.AddIfNotNullChecksum(this.SourceGeneratorExecutionVersionMap);
        this.FrozenSourceGeneratedDocumentIdentities?.AddAllTo(checksums);
        this.FrozenSourceGeneratedDocuments?.AddAllTo(checksums);
    }
 
    public void Serialize(ObjectWriter writer)
    {
        // Writing this is optional, but helps ensure checksums are being computed properly on both the host and oop side.
        this.Checksum.WriteTo(writer);
        this.SolutionState.WriteTo(writer);
        this.SourceGeneratorExecutionVersionMap.WriteTo(writer);
 
        // Write out a boolean to know whether we'll have this extra information
        writer.WriteBoolean(this.FrozenSourceGeneratedDocumentIdentities.HasValue);
        if (FrozenSourceGeneratedDocumentIdentities.HasValue)
        {
            this.FrozenSourceGeneratedDocuments!.Value.WriteTo(writer);
            this.FrozenSourceGeneratedDocumentIdentities.Value.WriteTo(writer);
            writer.WriteArray(this.FrozenSourceGeneratedDocumentGenerationDateTimes, static (w, d) => w.WriteInt64(d.Ticks));
        }
    }
 
    public static SolutionCompilationStateChecksums Deserialize(ObjectReader reader)
    {
        var checksum = Checksum.ReadFrom(reader);
        var solutionState = Checksum.ReadFrom(reader);
        var sourceGeneratorExecutionVersionMap = Checksum.ReadFrom(reader);
 
        var hasFrozenSourceGeneratedDocuments = reader.ReadBoolean();
        DocumentChecksumsAndIds? frozenSourceGeneratedDocumentTexts = null;
        ChecksumCollection? frozenSourceGeneratedDocumentIdentities = null;
        ImmutableArray<DateTime> frozenSourceGeneratedDocumentGenerationDateTimes = default;
 
        if (hasFrozenSourceGeneratedDocuments)
        {
            frozenSourceGeneratedDocumentTexts = DocumentChecksumsAndIds.ReadFrom(reader);
            frozenSourceGeneratedDocumentIdentities = ChecksumCollection.ReadFrom(reader);
            frozenSourceGeneratedDocumentGenerationDateTimes = reader.ReadArray(r => new DateTime(r.ReadInt64()));
        }
 
        var result = new SolutionCompilationStateChecksums(
            solutionState: solutionState,
            sourceGeneratorExecutionVersionMap: sourceGeneratorExecutionVersionMap,
            frozenSourceGeneratedDocumentTexts,
            frozenSourceGeneratedDocumentIdentities,
            frozenSourceGeneratedDocumentGenerationDateTimes);
        Contract.ThrowIfFalse(result.Checksum == checksum);
        return result;
    }
 
    public async Task FindAsync<TArg>(
        SolutionCompilationState compilationState,
        ProjectCone? projectCone,
        AssetPath assetPath,
        HashSet<Checksum> searchingChecksumsLeft,
        Action<Checksum, object, TArg> onAssetFound,
        TArg arg,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (searchingChecksumsLeft.Count == 0)
            return;
 
        if (assetPath.IncludeSolutionCompilationState)
        {
            if (assetPath.IncludeSolutionCompilationStateChecksums && searchingChecksumsLeft.Remove(this.Checksum))
                onAssetFound(this.Checksum, this, arg);
 
            if (assetPath.IncludeSolutionSourceGeneratorExecutionVersionMap && searchingChecksumsLeft.Remove(this.SourceGeneratorExecutionVersionMap))
            {
                // Only send over the part of the execution map corresponding to the project cone.
                var filteredExecutionMap = compilationState.GetFilteredSourceGenerationExecutionMap(projectCone);
                onAssetFound(this.SourceGeneratorExecutionVersionMap, filteredExecutionMap, arg);
            }
 
            if (compilationState.FrozenSourceGeneratedDocumentStates != null)
            {
                Contract.ThrowIfFalse(FrozenSourceGeneratedDocumentIdentities.HasValue);
                Contract.ThrowIfFalse(FrozenSourceGeneratedDocuments.HasValue);
 
                // This could either be the checksum for the text (which we'll use our regular helper for first)...
                if (assetPath.IncludeSolutionFrozenSourceGeneratedDocumentText)
                {
                    await ChecksumCollection.FindAsync(
                        new AssetPath(AssetPathKind.DocumentText, assetPath.ProjectId, assetPath.DocumentId),
                        compilationState.FrozenSourceGeneratedDocumentStates, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false);
                }
 
                // ... or one of the identities. In this case, we'll use the fact that there's a 1:1 correspondence between the
                // two collections we hold onto.
                if (assetPath.IncludeSolutionFrozenSourceGeneratedDocumentIdentities)
                {
                    var documentId = assetPath.DocumentId;
                    if (documentId != null)
                    {
                        // If the caller is asking for a specific document, we can just look it up directly.
                        var index = FrozenSourceGeneratedDocuments.Value.Ids.IndexOf(documentId);
                        if (index >= 0)
                        {
                            var identityChecksum = FrozenSourceGeneratedDocumentIdentities.Value.Children[index];
                            if (searchingChecksumsLeft.Remove(identityChecksum))
                            {
                                Contract.ThrowIfFalse(compilationState.FrozenSourceGeneratedDocumentStates.TryGetState(documentId, out var state));
                                onAssetFound(identityChecksum, state.Identity, arg);
                            }
                        }
                    }
                    else
                    {
                        // Otherwise, we'll have to search through all of them.
                        for (var i = 0; i < FrozenSourceGeneratedDocumentIdentities.Value.Count; i++)
                        {
                            var identityChecksum = FrozenSourceGeneratedDocumentIdentities.Value[0];
                            if (searchingChecksumsLeft.Remove(identityChecksum))
                            {
                                var id = FrozenSourceGeneratedDocuments.Value.Ids[i];
                                Contract.ThrowIfFalse(compilationState.FrozenSourceGeneratedDocumentStates.TryGetState(id, out var state));
                                onAssetFound(identityChecksum, state.Identity, arg);
                            }
                        }
                    }
                }
            }
        }
 
        var solutionState = compilationState.SolutionState;
        if (projectCone is null)
        {
            // If we're not in a project cone, start the search at the top most state-checksum corresponding to the
            // entire solution.
            Contract.ThrowIfFalse(solutionState.TryGetStateChecksums(out var solutionChecksums));
            await solutionChecksums.FindAsync(solutionState, projectCone, assetPath, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            // Otherwise, grab the top-most state checksum for this cone and search within that.
            Contract.ThrowIfFalse(solutionState.TryGetStateChecksums(projectCone.RootProjectId, out var solutionChecksums));
            await solutionChecksums.FindAsync(solutionState, projectCone, assetPath, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false);
        }
    }
}
 
/// <param name="projectConeId">The particular <see cref="ProjectId"/> if this was a checksum tree made for a particular
/// project cone.</param>
internal sealed class SolutionStateChecksums(
    ProjectId? projectConeId,
    Checksum attributes,
    ProjectChecksumsAndIds projects,
    ChecksumCollection analyzerReferences,
    Checksum fallbackAnalyzerOptionsChecksum)
{
    private ProjectCone? _projectCone;
 
    public Checksum Checksum { get; } = Checksum.Create(stackalloc[]
    {
        projectConeId == null ? Checksum.Null : projectConeId.Checksum,
        attributes,
        projects.Checksum,
        analyzerReferences.Checksum,
        fallbackAnalyzerOptionsChecksum,
    });
 
    public ProjectId? ProjectConeId { get; } = projectConeId;
    public Checksum Attributes { get; } = attributes;
    public ProjectChecksumsAndIds Projects { get; } = projects;
    public ChecksumCollection AnalyzerReferences { get; } = analyzerReferences;
    public Checksum FallbackAnalyzerOptions => fallbackAnalyzerOptionsChecksum;
 
    // Acceptably not threadsafe.  ProjectCone is a class, and the runtime guarantees anyone will see this field fully
    // initialized.  It's acceptable to have multiple instances of this in a race condition as the data will be same
    // (and our asserts don't check for reference equality, only value equality).
    public ProjectCone? ProjectCone => _projectCone ??= ComputeProjectCone();
 
    private ProjectCone? ComputeProjectCone()
        => ProjectConeId == null ? null : new ProjectCone(ProjectConeId, Projects.Ids.ToFrozenSet());
 
    public void AddAllTo(HashSet<Checksum> checksums)
    {
        checksums.AddIfNotNullChecksum(this.Checksum);
        checksums.AddIfNotNullChecksum(this.Attributes);
        this.Projects.Checksums.AddAllTo(checksums);
        this.AnalyzerReferences.AddAllTo(checksums);
        checksums.AddIfNotNullChecksum(this.FallbackAnalyzerOptions);
    }
 
    public void Serialize(ObjectWriter writer)
    {
        // Writing this is optional, but helps ensure checksums are being computed properly on both the host and oop side.
        this.Checksum.WriteTo(writer);
        writer.WriteBoolean(this.ProjectConeId != null);
        this.ProjectConeId?.WriteTo(writer);
 
        this.Attributes.WriteTo(writer);
        this.Projects.WriteTo(writer);
        this.AnalyzerReferences.WriteTo(writer);
        this.FallbackAnalyzerOptions.WriteTo(writer);
    }
 
    public static SolutionStateChecksums Deserialize(ObjectReader reader)
    {
        var checksum = Checksum.ReadFrom(reader);
 
        var result = new SolutionStateChecksums(
            projectConeId: reader.ReadBoolean() ? ProjectId.ReadFrom(reader) : null,
            attributes: Checksum.ReadFrom(reader),
            projects: ProjectChecksumsAndIds.ReadFrom(reader),
            analyzerReferences: ChecksumCollection.ReadFrom(reader),
            fallbackAnalyzerOptionsChecksum: Checksum.ReadFrom(reader));
        Contract.ThrowIfFalse(result.Checksum == checksum);
        return result;
    }
 
    public async Task FindAsync<TArg>(
        SolutionState solution,
        ProjectCone? projectCone,
        AssetPath assetPath,
        HashSet<Checksum> searchingChecksumsLeft,
        Action<Checksum, object, TArg> onAssetFound,
        TArg arg,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (searchingChecksumsLeft.Count == 0)
            return;
 
        if (assetPath.IncludeSolutionState)
        {
            if (assetPath.IncludeSolutionStateChecksums && searchingChecksumsLeft.Remove(Checksum))
                onAssetFound(Checksum, this, arg);
 
            if (assetPath.IncludeSolutionAttributes && searchingChecksumsLeft.Remove(Attributes))
                onAssetFound(Attributes, solution.SolutionAttributes, arg);
 
            if (assetPath.IncludeSolutionAnalyzerReferences)
                ChecksumCollection.Find(solution.AnalyzerReferences, AnalyzerReferences, searchingChecksumsLeft, onAssetFound, arg, cancellationToken);
 
            if (assetPath.IncludeSolutionFallbackAnalyzerOptions && searchingChecksumsLeft.Remove(FallbackAnalyzerOptions))
                onAssetFound(FallbackAnalyzerOptions, solution.FallbackAnalyzerOptions, arg);
        }
 
        if (searchingChecksumsLeft.Count == 0)
            return;
 
        if (assetPath.IncludeProjects || assetPath.IncludeDocuments)
        {
            if (assetPath.ProjectId is not null)
            {
                // Dive into this project to search for the remaining checksums.
                Contract.ThrowIfTrue(
                    projectCone != null && !projectCone.Contains(assetPath.ProjectId),
                    "Requesting an asset outside of the cone explicitly being asked for!");
 
                var projectState = solution.GetProjectState(assetPath.ProjectId);
                if (projectState != null &&
                    projectState.TryGetStateChecksums(out var projectStateChecksums))
                {
                    await projectStateChecksums.FindAsync(projectState, assetPath, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false);
                }
            }
            else
            {
                // Check all projects for the remaining checksums.
 
                foreach (var (projectId, projectState) in solution.ProjectStates)
                {
                    cancellationToken.ThrowIfCancellationRequested();
 
                    // If we have no more checksums, can immediately bail out.
                    if (searchingChecksumsLeft.Count == 0)
                        break;
 
                    if (projectCone != null && !projectCone.Contains(projectId))
                        continue;
 
                    // It's possible not all all our projects have checksums.  Specifically, we may have only been asked to
                    // compute the checksum tree for a subset of projects that were all that a feature needed.
                    if (!projectState.TryGetStateChecksums(out var projectStateChecksums))
                        continue;
 
                    await projectStateChecksums.FindAsync(projectState, assetPath, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false);
                }
            }
        }
    }
}
 
internal sealed class ProjectStateChecksums(
    ProjectId projectId,
    Checksum infoChecksum,
    Checksum compilationOptionsChecksum,
    Checksum parseOptionsChecksum,
    ChecksumCollection projectReferenceChecksums,
    ChecksumCollection metadataReferenceChecksums,
    ChecksumCollection analyzerReferenceChecksums,
    DocumentChecksumsAndIds documentChecksums,
    DocumentChecksumsAndIds additionalDocumentChecksums,
    DocumentChecksumsAndIds analyzerConfigDocumentChecksums) : IEquatable<ProjectStateChecksums>
{
    public Checksum Checksum { get; } = Checksum.Create(stackalloc[]
    {
        infoChecksum,
        compilationOptionsChecksum,
        parseOptionsChecksum,
        documentChecksums.Checksum,
        projectReferenceChecksums.Checksum,
        metadataReferenceChecksums.Checksum,
        analyzerReferenceChecksums.Checksum,
        additionalDocumentChecksums.Checksum,
        analyzerConfigDocumentChecksums.Checksum,
    });
 
    public ProjectId ProjectId => projectId;
 
    public Checksum Info => infoChecksum;
    public Checksum CompilationOptions => compilationOptionsChecksum;
    public Checksum ParseOptions => parseOptionsChecksum;
 
    public ChecksumCollection ProjectReferences => projectReferenceChecksums;
    public ChecksumCollection MetadataReferences => metadataReferenceChecksums;
    public ChecksumCollection AnalyzerReferences => analyzerReferenceChecksums;
 
    public DocumentChecksumsAndIds Documents => documentChecksums;
    public DocumentChecksumsAndIds AdditionalDocuments => additionalDocumentChecksums;
    public DocumentChecksumsAndIds AnalyzerConfigDocuments => analyzerConfigDocumentChecksums;
 
    public override bool Equals(object? obj)
        => Equals(obj as ProjectStateChecksums);
 
    public bool Equals(ProjectStateChecksums? obj)
        => this.Checksum == obj?.Checksum;
 
    public override int GetHashCode()
        => this.Checksum.GetHashCode();
 
    public void AddAllTo(HashSet<Checksum> checksums)
    {
        checksums.AddIfNotNullChecksum(this.Checksum);
        checksums.AddIfNotNullChecksum(this.Info);
        checksums.AddIfNotNullChecksum(this.CompilationOptions);
        checksums.AddIfNotNullChecksum(this.ParseOptions);
        this.ProjectReferences.AddAllTo(checksums);
        this.MetadataReferences.AddAllTo(checksums);
        this.AnalyzerReferences.AddAllTo(checksums);
        this.Documents.AddAllTo(checksums);
        this.AdditionalDocuments.AddAllTo(checksums);
        this.AnalyzerConfigDocuments.AddAllTo(checksums);
    }
 
    public void Serialize(ObjectWriter writer)
    {
        // Writing this is optional, but helps ensure checksums are being computed properly on both the host and oop side.
        this.Checksum.WriteTo(writer);
 
        this.ProjectId.WriteTo(writer);
        this.Info.WriteTo(writer);
        this.CompilationOptions.WriteTo(writer);
        this.ParseOptions.WriteTo(writer);
        this.ProjectReferences.WriteTo(writer);
        this.MetadataReferences.WriteTo(writer);
        this.AnalyzerReferences.WriteTo(writer);
        this.Documents.WriteTo(writer);
        this.AdditionalDocuments.WriteTo(writer);
        this.AnalyzerConfigDocuments.WriteTo(writer);
    }
 
    public static ProjectStateChecksums Deserialize(ObjectReader reader)
    {
        var checksum = Checksum.ReadFrom(reader);
        var result = new ProjectStateChecksums(
            projectId: ProjectId.ReadFrom(reader),
            infoChecksum: Checksum.ReadFrom(reader),
            compilationOptionsChecksum: Checksum.ReadFrom(reader),
            parseOptionsChecksum: Checksum.ReadFrom(reader),
            projectReferenceChecksums: ChecksumCollection.ReadFrom(reader),
            metadataReferenceChecksums: ChecksumCollection.ReadFrom(reader),
            analyzerReferenceChecksums: ChecksumCollection.ReadFrom(reader),
            documentChecksums: DocumentChecksumsAndIds.ReadFrom(reader),
            additionalDocumentChecksums: DocumentChecksumsAndIds.ReadFrom(reader),
            analyzerConfigDocumentChecksums: DocumentChecksumsAndIds.ReadFrom(reader));
        Contract.ThrowIfFalse(result.Checksum == checksum);
        return result;
    }
 
    public async Task FindAsync<TArg>(
        ProjectState state,
        AssetPath assetPath,
        HashSet<Checksum> searchingChecksumsLeft,
        Action<Checksum, object, TArg> onAssetFound,
        TArg arg,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        // verify input
        Contract.ThrowIfFalse(state.TryGetStateChecksums(out var stateChecksum));
        Contract.ThrowIfFalse(this == stateChecksum);
 
        if (searchingChecksumsLeft.Count == 0)
            return;
 
        if (assetPath.IncludeProjects)
        {
            if (assetPath.IncludeProjectStateChecksums && searchingChecksumsLeft.Remove(Checksum))
                onAssetFound(Checksum, this, arg);
 
            if (assetPath.IncludeProjectAttributes && searchingChecksumsLeft.Remove(Info))
                onAssetFound(Info, state.ProjectInfo.Attributes, arg);
 
            if (assetPath.IncludeProjectCompilationOptions && searchingChecksumsLeft.Remove(CompilationOptions))
            {
                var compilationOptions = state.CompilationOptions ?? throw new InvalidOperationException("We should not be trying to serialize a project with no compilation options; RemoteSupportedLanguages.IsSupported should have filtered it out.");
                onAssetFound(CompilationOptions, compilationOptions, arg);
            }
 
            if (assetPath.IncludeProjectParseOptions && searchingChecksumsLeft.Remove(ParseOptions))
            {
                var parseOptions = state.ParseOptions ?? throw new InvalidOperationException("We should not be trying to serialize a project with no parse options; RemoteSupportedLanguages.IsSupported should have filtered it out.");
                onAssetFound(ParseOptions, parseOptions, arg);
            }
 
            if (assetPath.IncludeProjectProjectReferences)
                ChecksumCollection.Find(state.ProjectReferences, ProjectReferences, searchingChecksumsLeft, onAssetFound, arg, cancellationToken);
 
            if (assetPath.IncludeProjectMetadataReferences)
                ChecksumCollection.Find(state.MetadataReferences, MetadataReferences, searchingChecksumsLeft, onAssetFound, arg, cancellationToken);
 
            if (assetPath.IncludeProjectAnalyzerReferences)
                ChecksumCollection.Find(state.AnalyzerReferences, AnalyzerReferences, searchingChecksumsLeft, onAssetFound, arg, cancellationToken);
        }
 
        if (assetPath.IncludeDocuments)
        {
            await ChecksumCollection.FindAsync(assetPath, state.DocumentStates, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false);
            await ChecksumCollection.FindAsync(assetPath, state.AdditionalDocumentStates, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false);
            await ChecksumCollection.FindAsync(assetPath, state.AnalyzerConfigDocumentStates, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false);
        }
    }
 
    public override string ToString()
        => $"""
            ProjectStateChecksums({ProjectId})
                Info={Info}
                CompilationOptions={CompilationOptions}
                ParseOptions={ParseOptions}
                ProjectReferences={ProjectReferences.Checksum}
                MetadataReferences={MetadataReferences.Checksum}
                AnalyzerReferences={AnalyzerReferences.Checksum}
                Documents={Documents.Checksum}
                AdditionalDocuments={AdditionalDocuments.Checksum}
                AnalyzerConfigDocuments={AnalyzerConfigDocuments.Checksum}
            """;
}
 
internal sealed class DocumentStateChecksums(
    DocumentId documentId,
    Checksum infoChecksum,
    Checksum textChecksum)
{
    public Checksum Checksum { get; } = Checksum.Create(infoChecksum, textChecksum);
 
    public DocumentId DocumentId => documentId;
    public Checksum Info => infoChecksum;
    public Checksum Text => textChecksum;
 
    public void AddAllTo(HashSet<Checksum> checksums)
    {
        checksums.AddIfNotNullChecksum(this.Info);
        checksums.AddIfNotNullChecksum(this.Text);
    }
 
    public async Task FindAsync<TArg>(
        AssetPath assetPath,
        TextDocumentState state,
        HashSet<Checksum> searchingChecksumsLeft,
        Action<Checksum, object, TArg> onAssetFound,
        TArg arg,
        CancellationToken cancellationToken)
    {
        Debug.Assert(state.TryGetStateChecksums(out var stateChecksum) && this == stateChecksum);
 
        cancellationToken.ThrowIfCancellationRequested();
 
        if (assetPath.IncludeDocumentAttributes && searchingChecksumsLeft.Remove(Info))
            onAssetFound(Info, state.Attributes, arg);
 
        if (assetPath.IncludeDocumentText && searchingChecksumsLeft.Remove(Text))
        {
            var text = await SerializableSourceText.FromTextDocumentStateAsync(state, cancellationToken).ConfigureAwait(false);
            onAssetFound(Text, text, arg);
        }
    }
 
    public override string ToString()
        => $"DocumentStateChecksums({DocumentId})";
}
 
/// <summary>
/// hold onto object checksum that currently doesn't have a place to hold onto checksum
/// </summary>
internal static class ChecksumCache
{
    public static Checksum GetOrCreate<TValue, TArg>(TValue value, Func<TValue, TArg, Checksum> checksumCreator, TArg arg)
        where TValue : class
    {
        return StronglyTypedChecksumCache<TValue, Checksum>.GetOrCreate(value, checksumCreator, arg);
    }
 
    public static ChecksumCollection GetOrCreateChecksumCollection<TReference>(
        ImmutableArray<TReference> references, ISerializerService serializer, CancellationToken cancellationToken) where TReference : class
    {
        // Grab the internal array from the immutable array.  This is safe as the callers can't modify it, and we just
        // want the internal reference object to use as the key in the cache.
        return GetOrCreateChecksumCollection(
            ImmutableCollectionsMarshal.AsArray(references)!, serializer, cancellationToken);
    }
 
    public static ChecksumCollection GetOrCreateChecksumCollection<TReference>(
        IReadOnlyList<TReference> references, ISerializerService serializer, CancellationToken cancellationToken) where TReference : class
    {
        // Cache both at the list-of-references level...
        return StronglyTypedChecksumCache<IReadOnlyList<TReference>, ChecksumCollection>.GetOrCreate(
            references,
            static (references, tuple) =>
            {
                var (serializer, cancellationToken) = tuple;
                var checksums = new FixedSizeArrayBuilder<Checksum>(references.Count);
                foreach (var reference in references)
                {
                    // ... and cache at the individual reference level.
                    var checksum = GetOrCreate(
                        reference,
                        static (reference, arg) =>
                        {
                            var (serializer, cancellationToken) = arg;
                            return serializer.CreateChecksum(reference, cancellationToken);
                        },
                        arg: (serializer, cancellationToken));
                    checksums.Add(checksum);
                }
 
                return new ChecksumCollection(checksums.MoveToImmutable());
            },
            (serializer, cancellationToken));
    }
 
    private static class StronglyTypedChecksumCache<TValue, TResult>
        where TValue : class
        where TResult : struct
    {
        private static readonly ConditionalWeakTable<TValue, StrongBox<TResult>> s_objectToChecksumCollectionCache = new();
 
        public static TResult GetOrCreate<TArg>(TValue value, Func<TValue, TArg, TResult> checksumCreator, TArg arg)
        {
            if (s_objectToChecksumCollectionCache.TryGetValue(value, out var checksumCollection))
                return checksumCollection.Value;
 
            return GetOrCreateSlow(value, checksumCreator, arg);
 
            static TResult GetOrCreateSlow(TValue value, Func<TValue, TArg, TResult> checksumCreator, TArg arg)
                => s_objectToChecksumCollectionCache.GetValue(value, _ => new StrongBox<TResult>(checksumCreator(value, arg))).Value;
        }
    }
}