File: Workspace\Solution\Project.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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
/// <summary>
/// Represents a project that is part of a <see cref="Solution"/>.
/// </summary>
[DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
public partial class Project
{
    private ImmutableDictionary<DocumentId, Document?> _idToDocumentMap = ImmutableDictionary<DocumentId, Document?>.Empty;
    private ImmutableDictionary<DocumentId, SourceGeneratedDocument> _idToSourceGeneratedDocumentMap = ImmutableDictionary<DocumentId, SourceGeneratedDocument>.Empty;
    private ImmutableDictionary<DocumentId, AdditionalDocument?> _idToAdditionalDocumentMap = ImmutableDictionary<DocumentId, AdditionalDocument?>.Empty;
    private ImmutableDictionary<DocumentId, AnalyzerConfigDocument?> _idToAnalyzerConfigDocumentMap = ImmutableDictionary<DocumentId, AnalyzerConfigDocument?>.Empty;
 
    internal Project(Solution solution, ProjectState projectState)
    {
        Contract.ThrowIfNull(solution);
        Contract.ThrowIfNull(projectState);
 
        Solution = solution;
        State = projectState;
    }
 
    internal ProjectState State { get; }
 
    /// <summary>
    /// The solution this project is part of.
    /// </summary>
    public Solution Solution { get; }
 
    /// <summary>
    /// The ID of the project. Multiple <see cref="Project"/> instances may share the same ID. However, only
    /// one project may have this ID in any given solution.
    /// </summary>
    public ProjectId Id => State.Id;
 
    /// <summary>
    /// The path to the project file or null if there is no project file.
    /// </summary>
    public string? FilePath => State.FilePath;
 
    /// <summary>
    /// The path to the output file, or null if it is not known.
    /// </summary>
    public string? OutputFilePath => State.OutputFilePath;
 
    /// <summary>
    /// The path to the reference assembly output file, or null if it is not known.
    /// </summary>
    public string? OutputRefFilePath => State.OutputRefFilePath;
 
    /// <summary>
    /// Compilation output file paths.
    /// </summary>
    public CompilationOutputInfo CompilationOutputInfo => State.CompilationOutputInfo;
 
    /// <summary>
    /// The default namespace of the project ("" if not defined, which means global namespace),
    /// or null if it is unknown or not applicable. 
    /// </summary>
    /// <remarks>
    /// Right now VB doesn't have the concept of "default namespace". But we conjure one in workspace 
    /// by assigning the value of the project's root namespace to it. So various feature can choose to 
    /// use it for their own purpose.
    /// In the future, we might consider officially exposing "default namespace" for VB project 
    /// (e.g. through a "defaultnamespace" msbuild property)
    /// </remarks>
    public string? DefaultNamespace => State.DefaultNamespace;
 
    /// <summary>
    /// <see langword="true"/> if this <see cref="Project"/> supports providing data through the
    /// <see cref="GetCompilationAsync(CancellationToken)"/> method.
    /// 
    /// If <see langword="false"/> then <see cref="GetCompilationAsync(CancellationToken)"/> method will return <see langword="null"/> instead.
    /// </summary>
    public bool SupportsCompilation => this.Services.GetService<ICompilationFactoryService>() != null;
 
    /// <summary>
    /// The language services from the host environment associated with this project's language.
    /// </summary>
    [Obsolete($"Use {nameof(Services)} instead.")]
#pragma warning disable CS0618 // Member is obsolete -- shouldn't be reported here https://github.com/dotnet/roslyn/issues/66409
    public HostLanguageServices LanguageServices => State.LanguageServices.HostLanguageServices;
#pragma warning restore
 
    /// <summary>
    /// Immutable snapshot of language services from the host environment associated with this project's language.
    /// Use this over <see cref="LanguageServices"/> when possible.
    /// </summary>
    public LanguageServices Services => State.LanguageServices;
 
    /// <summary>
    /// The language associated with the project.
    /// </summary>
    public string Language => State.Language;
 
    /// <summary>
    /// The name of the assembly this project represents.
    /// </summary>
    public string AssemblyName => State.AssemblyName;
 
    /// <summary>
    /// The name of the project. This may be different than the assembly name.
    /// </summary>
    public string Name => State.Name;
 
    /// <summary>
    /// The list of all other metadata sources (assemblies) that this project references.
    /// </summary>
    public IReadOnlyList<MetadataReference> MetadataReferences => State.MetadataReferences;
 
    /// <summary>
    /// The list of all other projects within the same solution that this project references.
    /// </summary>
    public IEnumerable<ProjectReference> ProjectReferences => State.ProjectReferences.Where(pr => this.Solution.ContainsProject(pr.ProjectId));
 
    /// <summary>
    /// The list of all other projects that this project references, including projects that 
    /// are not part of the solution.
    /// </summary>
    public IReadOnlyList<ProjectReference> AllProjectReferences => State.ProjectReferences;
 
    /// <summary>
    /// The list of all the diagnostic analyzer references for this project.
    /// </summary>
    public IReadOnlyList<AnalyzerReference> AnalyzerReferences => State.AnalyzerReferences;
 
    /// <summary>
    /// The options used by analyzers for this project.
    /// </summary>
    public AnalyzerOptions AnalyzerOptions => State.ProjectAnalyzerOptions;
 
    /// <summary>
    /// The options used by analyzers for this project.
    /// </summary>
    public AnalyzerOptions HostAnalyzerOptions => State.HostAnalyzerOptions;
 
    /// <summary>
    /// The options used when building the compilation for this project.
    /// </summary>
    public CompilationOptions? CompilationOptions => State.CompilationOptions;
 
    /// <summary>
    /// The options used when parsing documents for this project.
    /// </summary>
    public ParseOptions? ParseOptions => State.ParseOptions;
 
    /// <summary>
    /// Returns true if this is a submission project.
    /// </summary>
    public bool IsSubmission => State.IsSubmission;
 
    /// <summary>
    /// True if the project has any documents.
    /// </summary>
    public bool HasDocuments => !State.DocumentStates.IsEmpty;
 
    /// <summary>
    /// All the document IDs associated with this project.
    /// </summary>
    public IReadOnlyList<DocumentId> DocumentIds => State.DocumentStates.Ids;
 
    /// <summary>
    /// All the additional document IDs associated with this project.
    /// </summary>
    public IReadOnlyList<DocumentId> AdditionalDocumentIds => State.AdditionalDocumentStates.Ids;
 
    /// <summary>
    /// All the additional document IDs associated with this project.
    /// </summary>
    internal IReadOnlyList<DocumentId> AnalyzerConfigDocumentIds => State.AnalyzerConfigDocumentStates.Ids;
 
    /// <summary>
    /// All the regular documents associated with this project. Documents produced from source generators are returned by
    /// <see cref="GetSourceGeneratedDocumentsAsync(CancellationToken)"/>.
    /// </summary>
    public IEnumerable<Document> Documents => DocumentIds.Select(GetDocument)!;
 
    /// <summary>
    /// All the additional documents associated with this project.
    /// </summary>
    public IEnumerable<TextDocument> AdditionalDocuments => AdditionalDocumentIds.Select(GetAdditionalDocument)!;
 
    /// <summary>
    /// All the <see cref="AnalyzerConfigDocument"/>s associated with this project.
    /// </summary>
    public IEnumerable<AnalyzerConfigDocument> AnalyzerConfigDocuments => AnalyzerConfigDocumentIds.Select(GetAnalyzerConfigDocument)!;
 
    /// <summary>
    /// True if the project contains a document with the specified ID.
    /// </summary>
    public bool ContainsDocument(DocumentId documentId)
        => State.DocumentStates.Contains(documentId);
 
    /// <summary>
    /// True if the project contains an additional document with the specified ID.
    /// </summary>
    public bool ContainsAdditionalDocument(DocumentId documentId)
        => State.AdditionalDocumentStates.Contains(documentId);
 
    /// <summary>
    /// True if the project contains an <see cref="AnalyzerConfigDocument"/> with the specified ID.
    /// </summary>
    public bool ContainsAnalyzerConfigDocument(DocumentId documentId)
        => State.AnalyzerConfigDocumentStates.Contains(documentId);
 
    /// <summary>
    /// Get the documentId in this project with the specified syntax tree.
    /// </summary>
    public DocumentId? GetDocumentId(SyntaxTree? syntaxTree)
        => Solution.GetDocumentId(syntaxTree, this.Id);
 
    /// <summary>
    /// Get the document in this project with the specified syntax tree.
    /// </summary>
    public Document? GetDocument(SyntaxTree? syntaxTree)
        => Solution.GetDocument(syntaxTree, this.Id);
 
    /// <summary>
    /// Get the document in this project with the specified document Id.
    /// </summary>
    public Document? GetDocument(DocumentId documentId)
        => ImmutableInterlocked.GetOrAdd(ref _idToDocumentMap, documentId, s_tryCreateDocumentFunction, this);
 
    /// <summary>
    /// Get the additional document in this project with the specified document Id.
    /// </summary>
    public TextDocument? GetAdditionalDocument(DocumentId documentId)
        => ImmutableInterlocked.GetOrAdd(ref _idToAdditionalDocumentMap, documentId, s_tryCreateAdditionalDocumentFunction, this);
 
    /// <summary>
    /// Get the analyzer config document in this project with the specified document Id.
    /// </summary>
    public AnalyzerConfigDocument? GetAnalyzerConfigDocument(DocumentId documentId)
        => ImmutableInterlocked.GetOrAdd(ref _idToAnalyzerConfigDocumentMap, documentId, s_tryCreateAnalyzerConfigDocumentFunction, this);
 
    /// <summary>
    /// Gets a document or a source generated document in this solution with the specified document ID.
    /// </summary>
    internal async ValueTask<Document?> GetDocumentAsync(DocumentId documentId, bool includeSourceGenerated = false, CancellationToken cancellationToken = default)
    {
        var document = GetDocument(documentId);
        if (document != null || !includeSourceGenerated)
        {
            return document;
        }
 
        return await GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Gets a document, additional document, analyzer config document or a source generated document in this solution with the specified document ID.
    /// </summary>
    internal async ValueTask<TextDocument?> GetTextDocumentAsync(DocumentId documentId, CancellationToken cancellationToken = default)
    {
        var document = GetDocument(documentId) ?? GetAdditionalDocument(documentId) ?? GetAnalyzerConfigDocument(documentId);
        if (document != null)
            return document;
 
        return await GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Gets all source generated documents in this project.
    /// </summary>
    public async ValueTask<IEnumerable<SourceGeneratedDocument>> GetSourceGeneratedDocumentsAsync(CancellationToken cancellationToken = default)
    {
        var generatedDocumentStates = await Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(this.State, cancellationToken).ConfigureAwait(false);
 
        // return an iterator to avoid eagerly allocating all the document instances
        return generatedDocumentStates.States.Values.Select(state =>
            ImmutableInterlocked.GetOrAdd(ref _idToSourceGeneratedDocumentMap, state.Id, s_createSourceGeneratedDocumentFunction, (state, this)));
    }
 
    internal async IAsyncEnumerable<Document> GetAllRegularAndSourceGeneratedDocumentsAsync([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        foreach (var document in this.Documents)
            yield return document;
 
        foreach (var document in await GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false))
            yield return document;
    }
 
    public async ValueTask<SourceGeneratedDocument?> GetSourceGeneratedDocumentAsync(DocumentId documentId, CancellationToken cancellationToken = default)
    {
        // Immediately shortcircuit out if we know this is not a doc-id corresponding to an SG document.
        if (!documentId.IsSourceGenerated)
            return null;
 
        // User incorrect called into us with a doc id for a different project.  Ideally we'd throw here, but we've
        // always been resilient to this misuse since the start of roslyn, so we just quick-bail instead.
        if (this.Id != documentId.ProjectId)
            return null;
 
        // Quick check first: if we already have created a SourceGeneratedDocument wrapper, we're good
        if (_idToSourceGeneratedDocumentMap.TryGetValue(documentId, out var sourceGeneratedDocument))
            return sourceGeneratedDocument;
 
        // We'll have to run generators if we haven't already and now try to find it.
        var generatedDocumentStates = await Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(State, cancellationToken).ConfigureAwait(false);
        var generatedDocumentState = generatedDocumentStates.GetState(documentId);
        if (generatedDocumentState is null)
            return null;
 
        return GetOrCreateSourceGeneratedDocument(generatedDocumentState);
    }
 
    internal SourceGeneratedDocument GetOrCreateSourceGeneratedDocument(SourceGeneratedDocumentState state)
        => ImmutableInterlocked.GetOrAdd(ref _idToSourceGeneratedDocumentMap, state.Id, s_createSourceGeneratedDocumentFunction, (state, this));
 
    /// <summary>
    /// Returns the <see cref="SourceGeneratedDocumentState"/> for a source generated document that has already been generated and observed.
    /// </summary>
    /// <remarks>
    /// This is only safe to call if you already have seen the SyntaxTree or equivalent that indicates the document state has already been
    /// generated. This method exists to implement <see cref="Solution.GetDocument(SyntaxTree?)"/> and is best avoided unless you're doing something
    /// similarly tricky like that.
    /// </remarks>
    internal SourceGeneratedDocument? TryGetSourceGeneratedDocumentForAlreadyGeneratedId(DocumentId documentId)
    {
        // Immediately shortcircuit out if we know this is not a doc-id corresponding to an SG document.
        if (!documentId.IsSourceGenerated)
            return null;
 
        // User incorrect called into us with a doc id for a different project.  Ideally we'd throw here, but we've
        // always been resilient to this misuse since the start of roslyn, so we just quick-bail instead.
        if (this.Id != documentId.ProjectId)
            return null;
 
        // Easy case: do we already have the SourceGeneratedDocument created?
        if (_idToSourceGeneratedDocumentMap.TryGetValue(documentId, out var document))
            return document;
 
        // Trickier case now: it's possible we generated this, but we don't actually have the SourceGeneratedDocument for it, so let's go
        // try to fetch the state.
        var documentState = Solution.CompilationState.TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(documentId);
        if (documentState == null)
            return null;
 
        return ImmutableInterlocked.GetOrAdd(ref _idToSourceGeneratedDocumentMap, documentId, s_createSourceGeneratedDocumentFunction, (documentState, this));
    }
 
    internal ValueTask<ImmutableArray<Diagnostic>> GetSourceGeneratorDiagnosticsAsync(CancellationToken cancellationToken)
    {
        return Solution.CompilationState.GetSourceGeneratorDiagnosticsAsync(this.State, cancellationToken);
    }
 
    internal ValueTask<GeneratorDriverRunResult?> GetSourceGeneratorRunResultAsync(CancellationToken cancellationToken)
    {
        return Solution.CompilationState.GetSourceGeneratorRunResultAsync(this.State, cancellationToken);
    }
 
    internal Task<bool> ContainsSymbolsWithNameAsync(
        string name, CancellationToken cancellationToken)
    {
        return ContainsSymbolsAsync(
            (index, cancellationToken) => index.ProbablyContainsIdentifier(name) || index.ProbablyContainsEscapedIdentifier(name),
            cancellationToken);
    }
 
    internal Task<bool> ContainsSymbolsWithNameAsync(
        string name, SymbolFilter filter, CancellationToken cancellationToken)
    {
        return ContainsSymbolsWithNameAsync(
            typeName => name == typeName,
            filter,
            cancellationToken);
    }
 
    internal Task<bool> ContainsSymbolsWithNameAsync(
        Func<string, bool> predicate, SymbolFilter filter, CancellationToken cancellationToken)
    {
        return ContainsDeclarationAsync(
            (index, cancellationToken) =>
            {
                foreach (var info in index.DeclaredSymbolInfos)
                {
                    if (FilterMatches(info, filter) && predicate(info.Name))
                        return true;
                }
 
                return false;
            },
            cancellationToken);
 
        static bool FilterMatches(DeclaredSymbolInfo info, SymbolFilter filter)
        {
            switch (info.Kind)
            {
                case DeclaredSymbolInfoKind.Namespace:
                    return (filter & SymbolFilter.Namespace) != 0;
                case DeclaredSymbolInfoKind.Class:
                case DeclaredSymbolInfoKind.Delegate:
                case DeclaredSymbolInfoKind.Enum:
                case DeclaredSymbolInfoKind.Interface:
                case DeclaredSymbolInfoKind.Module:
                case DeclaredSymbolInfoKind.Record:
                case DeclaredSymbolInfoKind.RecordStruct:
                case DeclaredSymbolInfoKind.Struct:
                    return (filter & SymbolFilter.Type) != 0;
                case DeclaredSymbolInfoKind.Constant:
                case DeclaredSymbolInfoKind.Constructor:
                case DeclaredSymbolInfoKind.EnumMember:
                case DeclaredSymbolInfoKind.Event:
                case DeclaredSymbolInfoKind.ExtensionMethod:
                case DeclaredSymbolInfoKind.Field:
                case DeclaredSymbolInfoKind.Indexer:
                case DeclaredSymbolInfoKind.Method:
                case DeclaredSymbolInfoKind.Property:
                    return (filter & SymbolFilter.Member) != 0;
                default:
                    throw ExceptionUtilities.UnexpectedValue(info.Kind);
            }
        }
    }
 
    private Task<bool> ContainsSymbolsAsync(
        Func<SyntaxTreeIndex, CancellationToken, bool> predicate, CancellationToken cancellationToken)
    {
        return ContainsAsync(async d =>
        {
            var index = await SyntaxTreeIndex.GetRequiredIndexAsync(d, cancellationToken).ConfigureAwait(false);
            return predicate(index, cancellationToken);
        });
    }
 
    private Task<bool> ContainsDeclarationAsync(
        Func<TopLevelSyntaxTreeIndex, CancellationToken, bool> predicate, CancellationToken cancellationToken)
    {
        return ContainsAsync(async d =>
        {
            var index = await TopLevelSyntaxTreeIndex.GetRequiredIndexAsync(d, cancellationToken).ConfigureAwait(false);
            return predicate(index, cancellationToken);
        });
    }
 
    private async Task<bool> ContainsAsync(Func<Document, Task<bool>> predicateAsync)
    {
        if (!this.SupportsCompilation)
            return false;
 
        var results = await Task.WhenAll(this.Documents.Select(predicateAsync)).ConfigureAwait(false);
        return results.Any(b => b);
    }
 
    private static readonly Func<DocumentId, Project, Document?> s_tryCreateDocumentFunction =
        (documentId, project) => project.State.DocumentStates.TryGetState(documentId, out var state) ? new Document(project, state) : null;
 
    private static readonly Func<DocumentId, Project, AdditionalDocument?> s_tryCreateAdditionalDocumentFunction =
        (documentId, project) => project.State.AdditionalDocumentStates.TryGetState(documentId, out var state) ? new AdditionalDocument(project, state) : null;
 
    private static readonly Func<DocumentId, Project, AnalyzerConfigDocument?> s_tryCreateAnalyzerConfigDocumentFunction =
        (documentId, project) => project.State.AnalyzerConfigDocumentStates.TryGetState(documentId, out var state) ? new AnalyzerConfigDocument(project, state) : null;
 
    private static readonly Func<DocumentId, (SourceGeneratedDocumentState state, Project project), SourceGeneratedDocument> s_createSourceGeneratedDocumentFunction =
        (documentId, stateAndProject) => new SourceGeneratedDocument(stateAndProject.project, stateAndProject.state);
 
    /// <summary>
    /// Tries to get the cached <see cref="Compilation"/> for this project if it has already been created and is still cached. In almost all
    /// cases you should call <see cref="GetCompilationAsync"/> which will either return the cached <see cref="Compilation"/>
    /// or create a new one otherwise.
    /// </summary>
    public bool TryGetCompilation([NotNullWhen(returnValue: true)] out Compilation? compilation)
        => Solution.CompilationState.TryGetCompilation(this.Id, out compilation);
 
    /// <summary>
    /// Get the <see cref="Compilation"/> for this project asynchronously.
    /// </summary>
    /// <returns>
    /// Returns the produced <see cref="Compilation"/>, or <see langword="null"/> if <see
    /// cref="SupportsCompilation"/> returns <see langword="false"/>. This function will
    /// return the same value if called multiple times.
    /// </returns>
    public Task<Compilation?> GetCompilationAsync(CancellationToken cancellationToken = default)
        => Solution.CompilationState.GetCompilationAsync(State, cancellationToken);
 
    /// <summary>
    /// Determines if the compilation returned by <see cref="GetCompilationAsync"/> and all its referenced compilation are from fully loaded projects.
    /// </summary>
    // TODO: make this public
    internal Task<bool> HasSuccessfullyLoadedAsync(CancellationToken cancellationToken)
        => Solution.CompilationState.HasSuccessfullyLoadedAsync(State, cancellationToken);
 
    /// <summary>
    /// Gets an object that lists the added, changed and removed documents between this project and the specified project.
    /// </summary>
    public ProjectChanges GetChanges(Project oldProject)
    {
        if (oldProject == null)
        {
            throw new ArgumentNullException(nameof(oldProject));
        }
 
        return new ProjectChanges(this, oldProject);
    }
 
    /// <summary>
    /// The project version. This equates to the version of the project file.
    /// </summary>
    public VersionStamp Version => State.Version;
 
    /// <summary>
    /// The version of the most recently modified document.
    /// </summary>
    public Task<VersionStamp> GetLatestDocumentVersionAsync(CancellationToken cancellationToken = default)
        => State.GetLatestDocumentVersionAsync(cancellationToken);
 
    /// <summary>
    /// The most recent version of the project, its documents and all dependent projects and documents.
    /// </summary>
    public Task<VersionStamp> GetDependentVersionAsync(CancellationToken cancellationToken = default)
        => Solution.CompilationState.GetDependentVersionAsync(this.Id, cancellationToken);
 
    /// <summary>
    /// The semantic version of this project including the semantics of referenced projects.
    /// This version changes whenever the consumable declarations of this project and/or projects it depends on change.
    /// </summary>
    public Task<VersionStamp> GetDependentSemanticVersionAsync(CancellationToken cancellationToken = default)
        => Solution.CompilationState.GetDependentSemanticVersionAsync(this.Id, cancellationToken);
 
    /// <summary>
    /// The semantic version of this project not including the semantics of referenced projects.
    /// This version changes only when the consumable declarations of this project change.
    /// </summary>
    public Task<VersionStamp> GetSemanticVersionAsync(CancellationToken cancellationToken = default)
        => State.GetSemanticVersionAsync(cancellationToken);
 
    /// <summary>
    /// Calculates a checksum that contains a project's checksum along with a checksum for each of the project's 
    /// transitive dependencies.
    /// </summary>
    /// <remarks>
    /// This checksum calculation can be used for cases where a feature needs to know if the semantics in this project
    /// changed.  For example, for diagnostics or caching computed semantic data. The goal is to ensure that changes to
    /// <list type="bullet">
    ///    <item>Files inside the current project</item>
    ///    <item>Project properties of the current project</item>
    ///    <item>Visible files in referenced projects</item>
    ///    <item>Project properties in referenced projects</item>
    /// </list>
    /// are reflected in the metadata we keep so that comparing solutions accurately tells us when we need to recompute
    /// semantic work.   
    /// 
    /// <para>This method of checking for changes has a few important properties that differentiate it from other methods of determining project version.
    /// <list type="bullet">
    ///    <item>Changes to methods inside the current project will be reflected to compute updated diagnostics.
    ///        <see cref="Project.GetDependentSemanticVersionAsync(CancellationToken)"/> does not change as it only returns top level changes.</item>
    ///    <item>Reloading a project without making any changes will re-use cached diagnostics.
    ///        <see cref="Project.GetDependentSemanticVersionAsync(CancellationToken)"/> changes as the project is removed, then added resulting in a version change.</item>
    /// </list>   
    /// </para>
    /// </remarks>
    internal Task<Checksum> GetDependentChecksumAsync(CancellationToken cancellationToken)
        => Solution.CompilationState.GetDependentChecksumAsync(this.Id, cancellationToken);
 
    /// <summary>
    /// Creates a new instance of this project updated to have the new assembly name.
    /// </summary>
    public Project WithAssemblyName(string assemblyName)
        => this.Solution.WithProjectAssemblyName(this.Id, assemblyName).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to have the new default namespace.
    /// </summary>
    public Project WithDefaultNamespace(string defaultNamespace)
        => this.Solution.WithProjectDefaultNamespace(this.Id, defaultNamespace).GetRequiredProject(Id);
 
    internal Project WithOutputFilePath(string? path)
        => Solution.WithProjectOutputFilePath(Id, path).GetRequiredProject(Id);
 
    internal Project WithOutputRefFilePath(string? path)
        => Solution.WithProjectOutputRefFilePath(Id, path).GetRequiredProject(Id);
 
    internal Project WithCompilationOutputInfo(in CompilationOutputInfo info)
        => Solution.WithProjectCompilationOutputInfo(Id, info).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to have the specified compilation options.
    /// </summary>
    public Project WithCompilationOptions(CompilationOptions options)
        => this.Solution.WithProjectCompilationOptions(this.Id, options).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to have the specified parse options.
    /// </summary>
    public Project WithParseOptions(ParseOptions options)
        => this.Solution.WithProjectParseOptions(this.Id, options).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to include the specified project reference
    /// in addition to already existing ones.
    /// </summary>
    public Project AddProjectReference(ProjectReference projectReference)
        => this.Solution.AddProjectReference(this.Id, projectReference).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to include the specified project references
    /// in addition to already existing ones.
    /// </summary>
    public Project AddProjectReferences(IEnumerable<ProjectReference> projectReferences)
        => this.Solution.AddProjectReferences(this.Id, projectReferences).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to no longer include the specified project reference.
    /// </summary>
    public Project RemoveProjectReference(ProjectReference projectReference)
        => this.Solution.RemoveProjectReference(this.Id, projectReference).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to replace existing project references 
    /// with the specified ones.
    /// </summary>
    public Project WithProjectReferences(IEnumerable<ProjectReference> projectReferences)
        => this.Solution.WithProjectReferences(this.Id, projectReferences).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to include the specified metadata reference
    /// in addition to already existing ones.
    /// </summary>
    public Project AddMetadataReference(MetadataReference metadataReference)
        => this.Solution.AddMetadataReference(this.Id, metadataReference).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to include the specified metadata references
    /// in addition to already existing ones.
    /// </summary>
    public Project AddMetadataReferences(IEnumerable<MetadataReference> metadataReferences)
        => this.Solution.AddMetadataReferences(this.Id, metadataReferences).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to no longer include the specified metadata reference.
    /// </summary>
    public Project RemoveMetadataReference(MetadataReference metadataReference)
        => this.Solution.RemoveMetadataReference(this.Id, metadataReference).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to replace existing metadata reference
    /// with the specified ones.
    /// </summary>
    public Project WithMetadataReferences(IEnumerable<MetadataReference> metadataReferences)
        => this.Solution.WithProjectMetadataReferences(this.Id, metadataReferences).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to include the specified analyzer reference 
    /// in addition to already existing ones.
    /// </summary>
    public Project AddAnalyzerReference(AnalyzerReference analyzerReference)
        => this.Solution.AddAnalyzerReference(this.Id, analyzerReference).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to include the specified analyzer references
    /// in addition to already existing ones.
    /// </summary>
    public Project AddAnalyzerReferences(IEnumerable<AnalyzerReference> analyzerReferences)
        => this.Solution.AddAnalyzerReferences(this.Id, analyzerReferences).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to no longer include the specified analyzer reference.
    /// </summary>
    public Project RemoveAnalyzerReference(AnalyzerReference analyzerReference)
        => this.Solution.RemoveAnalyzerReference(this.Id, analyzerReference).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to replace existing analyzer references 
    /// with the specified ones.
    /// </summary>
    public Project WithAnalyzerReferences(IEnumerable<AnalyzerReference> analyzerReferencs)
        => this.Solution.WithProjectAnalyzerReferences(this.Id, analyzerReferencs).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to replace existing analyzer references 
    /// with the specified ones.
    /// </summary>
    internal Project WithAttributes(ProjectInfo.ProjectAttributes attributes)
        => Solution.WithProjectAttributes(attributes).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new document in a new instance of this project.
    /// </summary>
    public Document AddDocument(string name, SyntaxNode syntaxRoot, IEnumerable<string>? folders = null, string? filePath = null)
    {
        var id = DocumentId.CreateNewId(this.Id);
 
        // use preserve identity for forked solution directly from syntax node.
        // this lets us not serialize temporary tree unnecessarily
        return this.Solution.AddDocument(id, name, syntaxRoot, folders, filePath, preservationMode: PreservationMode.PreserveIdentity).GetDocument(id)!;
    }
 
    /// <summary>
    /// Creates a new document in a new instance of this project.
    /// </summary>
    public Document AddDocument(string name, SourceText text, IEnumerable<string>? folders = null, string? filePath = null)
    {
        var id = DocumentId.CreateNewId(this.Id);
        return this.Solution.AddDocument(id, name, text, folders, filePath).GetDocument(id)!;
    }
 
    /// <summary>
    /// Creates a new document in a new instance of this project.
    /// </summary>
    public Document AddDocument(string name, string text, IEnumerable<string>? folders = null, string? filePath = null)
    {
        var id = DocumentId.CreateNewId(this.Id, debugName: name);
        return this.Solution.AddDocument(id, name, text, folders, filePath).GetDocument(id)!;
    }
 
    /// <summary>
    /// Creates a new additional document in a new instance of this project.
    /// </summary>
    public TextDocument AddAdditionalDocument(string name, SourceText text, IEnumerable<string>? folders = null, string? filePath = null)
    {
        var id = DocumentId.CreateNewId(this.Id);
        return this.Solution.AddAdditionalDocument(id, name, text, folders, filePath).GetAdditionalDocument(id)!;
    }
 
    /// <summary>
    /// Creates a new additional document in a new instance of this project.
    /// </summary>
    public TextDocument AddAdditionalDocument(string name, string text, IEnumerable<string>? folders = null, string? filePath = null)
    {
        var id = DocumentId.CreateNewId(this.Id);
        return this.Solution.AddAdditionalDocument(id, name, text, folders, filePath).GetAdditionalDocument(id)!;
    }
 
    /// <summary>
    /// Creates a new analyzer config document in a new instance of this project.
    /// </summary>
    public TextDocument AddAnalyzerConfigDocument(string name, SourceText text, IEnumerable<string>? folders = null, string? filePath = null)
    {
        var id = DocumentId.CreateNewId(this.Id);
        return this.Solution.AddAnalyzerConfigDocument(id, name, text, folders, filePath).GetAnalyzerConfigDocument(id)!;
    }
 
    /// <summary>
    /// Creates a new instance of this project updated to no longer include the specified document.
    /// </summary>
    public Project RemoveDocument(DocumentId documentId)
    {
        // NOTE: the method isn't checking if documentId belongs to the project. This probably should be done, but may be a compat change.
        // https://github.com/dotnet/roslyn/issues/41211 tracks this investigation.
        return this.Solution.RemoveDocument(documentId).GetRequiredProject(Id);
    }
 
    /// <summary>
    /// Creates a new instance of this project updated to no longer include the specified documents.
    /// </summary>
    public Project RemoveDocuments(ImmutableArray<DocumentId> documentIds)
    {
        CheckIdsContainedInProject(documentIds);
 
        return this.Solution.RemoveDocuments(documentIds).GetRequiredProject(this.Id);
    }
 
    /// <summary>
    /// Creates a new instance of this project updated to no longer include the specified additional document.
    /// </summary>
    public Project RemoveAdditionalDocument(DocumentId documentId)
        // NOTE: the method isn't checking if documentId belongs to the project. This probably should be done, but may be a compat change.
        // https://github.com/dotnet/roslyn/issues/41211 tracks this investigation.
        => this.Solution.RemoveAdditionalDocument(documentId).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new instance of this project updated to no longer include the specified additional documents.
    /// </summary>
    public Project RemoveAdditionalDocuments(ImmutableArray<DocumentId> documentIds)
    {
        CheckIdsContainedInProject(documentIds);
 
        return this.Solution.RemoveAdditionalDocuments(documentIds).GetRequiredProject(this.Id);
    }
 
    /// <summary>
    /// Creates a new instance of this project updated to no longer include the specified analyzer config document.
    /// </summary>
    public Project RemoveAnalyzerConfigDocument(DocumentId documentId)
        // NOTE: the method isn't checking if documentId belongs to the project. This probably should be done, but may be a compat change.
        // https://github.com/dotnet/roslyn/issues/41211 tracks this investigation.
        => this.Solution.RemoveAnalyzerConfigDocument(documentId).GetRequiredProject(Id);
 
    /// <summary>
    /// Creates a new solution instance that no longer includes the specified <see cref="AnalyzerConfigDocument"/>s.
    /// </summary>
    public Project RemoveAnalyzerConfigDocuments(ImmutableArray<DocumentId> documentIds)
    {
        CheckIdsContainedInProject(documentIds);
 
        return this.Solution.RemoveAnalyzerConfigDocuments(documentIds).GetRequiredProject(this.Id);
    }
 
    private void CheckIdsContainedInProject(ImmutableArray<DocumentId> documentIds)
    {
        foreach (var documentId in documentIds)
        {
            // Dealing with nulls is handled by the caller of this
            if (documentId?.ProjectId != this.Id)
            {
                throw new ArgumentException(string.Format(WorkspacesResources._0_is_in_a_different_project, documentId));
            }
        }
    }
 
    internal AnalyzerConfigData? GetAnalyzerConfigOptions()
        => State.GetAnalyzerConfigOptions();
 
    /// <summary>
    /// Retrieves fallback analyzer options for this project's language.
    /// </summary>
    internal StructuredAnalyzerConfigOptions GetFallbackAnalyzerOptions()
        => Solution.FallbackAnalyzerOptions.GetValueOrDefault(Language, StructuredAnalyzerConfigOptions.Empty);
 
    private string GetDebuggerDisplay()
        => this.Name;
 
    internal SkippedHostAnalyzersInfo GetSkippedAnalyzersInfo(DiagnosticAnalyzerInfoCache infoCache)
        => Solution.SolutionState.Analyzers.GetSkippedAnalyzersInfo(this, infoCache);
 
    internal async ValueTask<Document?> GetDocumentAsync(ImmutableArray<byte> contentHash, CancellationToken cancellationToken)
    {
        var documentId = await State.GetDocumentIdAsync(contentHash, cancellationToken).ConfigureAwait(false);
        return documentId is null ? null : GetDocument(documentId);
    }
}