File: Workspace\Solution\SolutionState.SymbolToProjectId.cs
Web Access
Project: ..\..\..\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.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    internal partial class SolutionState
    {
        /// <inheritdoc cref="Solution.GetOriginatingProjectId"/>
        public ProjectId? GetOriginatingProjectId(ISymbol? symbol)
        {
            if (symbol == null)
                return null;
 
            var projectId = GetOriginatingProjectIdWorker(symbol);
 
            // Validate some invariants we think should hold.  We want to know if this breaks, which indicates some part
            // of our system not working as we might expect.  If they break, create NFWs so we can find out and
            // investigate.
 
            if (SymbolKey.IsBodyLevelSymbol(symbol))
            {
                // If this is a method-body-level symbol, then we will have it's syntax tree.  Since  we already have a
                // mapping from syntax-trees to docs, so we can immediately map this back to it's originating project.
                //
                // Note: we don't do this for all source symbols, only method-body-level ones.  That's because other
                // source symbols may be *retargetted*.  So you can have the same symbol retargetted into multiple
                // projects, but which have the same syntax-tree (which is only in one project).  We need to actually
                // check it's assembly symbol so that we get the actual project it is from (the original project, or the
                // retargetted project).
                var syntaxTree = symbol.Locations[0].SourceTree;
                Contract.ThrowIfNull(syntaxTree);
 
                var documentId = this.GetDocumentState(syntaxTree, projectId: null)?.Id;
                if (documentId == null)
                {
                    try
                    {
                        throw new InvalidOperationException(
                            $"We should always be able to map a body symbol back to a document:\r\n{symbol.Kind}\r\n{symbol.Name}\r\n{syntaxTree.FilePath}\r\n{projectId}");
                    }
                    catch (Exception ex) when (FatalError.ReportAndCatch(ex))
                    {
                    }
                }
                else if (documentId.ProjectId != projectId)
                {
                    try
                    {
                        throw new InvalidOperationException(
                            $"Syntax tree for a body symbol should map to the same project as the body symbol's assembly:\r\n{symbol.Kind}\r\n{symbol.Name}\r\n{syntaxTree.FilePath}\r\n{projectId}\r\n{documentId.ProjectId}");
                    }
                    catch (Exception ex) when (FatalError.ReportAndCatch(ex))
                    {
                    }
                }
            }
 
            return projectId;
        }
 
        private ProjectId? GetOriginatingProjectIdWorker(ISymbol symbol)
        {
            LazyInitialization.EnsureInitialized(ref _unrootedSymbolToProjectId, s_createTable);
 
            // Walk up the symbol so we can get to the containing namespace/assembly that will be used to map
            // back to a project.
 
            while (symbol != null)
            {
                var result = GetProjectIdDirectly(symbol, _unrootedSymbolToProjectId);
                if (result != null)
                    return result;
 
                symbol = symbol.ContainingSymbol;
            }
 
            return null;
        }
 
        private ProjectId? GetProjectIdDirectly(
            ISymbol symbol, ConditionalWeakTable<ISymbol, ProjectId?> unrootedSymbolToProjectId)
        {
            if (symbol.IsKind(SymbolKind.Namespace, out INamespaceSymbol? ns))
            {
                if (ns.ContainingCompilation != null)
                {
                    // A namespace that spans a compilation.  These don't belong to an assembly/module directly.
                    // However, as we're looking for the project this corresponds to, we can look for the
                    // source-module component (the first in the constituent namespaces) and then search using that.
                    return GetOriginatingProjectId(ns.ConstituentNamespaces[0]);
                }
            }
            else if (symbol.IsKind(SymbolKind.Assembly) ||
                     symbol.IsKind(SymbolKind.NetModule) ||
                     symbol.IsKind(SymbolKind.DynamicType))
            {
                if (!unrootedSymbolToProjectId.TryGetValue(symbol, out var projectId))
                {
                    // First, look through all the projects, and see if this symbol came from the primary assembly for
                    // that project.  (i.e. was a source symbol from that project, or was retargetted into that
                    // project).  If so, that's the originating project.
                    //
                    // If we don't find any primary results, then look through all the secondary assemblies (i.e.
                    // references) for that project.  This is the case for metadata symbols.  A metadata symbol might be
                    // found in many projects, so we just return the first result as that's just as good for finding the
                    // metadata symbol as any other project.
                    projectId = TryGetProjectId(symbol, primary: true) ??
                                TryGetProjectId(symbol, primary: false);
 
                    // Have to lock as there's no atomic AddOrUpdate in netstandard2.0 and we could throw if two
                    // threads tried to add the same item.
#if !NETCOREAPP
                    lock (unrootedSymbolToProjectId)
                    {
                        unrootedSymbolToProjectId.Remove(symbol);
                        unrootedSymbolToProjectId.Add(symbol, projectId);
                    }
#else
                    unrootedSymbolToProjectId.AddOrUpdate(symbol, projectId);
#endif
                }
 
                return projectId;
            }
            else if (symbol.IsKind(SymbolKind.TypeParameter, out ITypeParameterSymbol? typeParameter) &&
                     typeParameter.TypeParameterKind == TypeParameterKind.Cref)
            {
                // Cref type parameters don't belong to any containing symbol.  But we can map them to a doc/project
                // using the declaring syntax of the type parameter itself.
                var tree = typeParameter.Locations[0].SourceTree;
                var doc = this.GetDocumentState(tree, projectId: null);
                return doc?.Id.ProjectId;
            }
 
            return null;
 
            ProjectId? TryGetProjectId(ISymbol symbol, bool primary)
            {
                foreach (var (id, tracker) in _projectIdToTrackerMap)
                {
                    if (tracker.ContainsAssemblyOrModuleOrDynamic(symbol, primary))
                        return id;
                }
 
                return null;
            }
        }
    }
}