File: ProjectSystem\Legacy\AbstractLegacyProject.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Microsoft.VisualStudio.LanguageServices.ProjectSystem;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Legacy;
 
/// <summary>
/// Base type for legacy C# and VB project system shim implementations.
/// These legacy shims are based on legacy project system interfaces defined in csproj/msvbprj.
/// </summary>
internal abstract partial class AbstractLegacyProject
{
    public IVsHierarchy Hierarchy { get; }
    protected ProjectSystemProject ProjectSystemProject { get; }
    internal ProjectSystemProjectOptionsProcessor ProjectSystemProjectOptionsProcessor { get; set; }
    protected IProjectCodeModel ProjectCodeModel { get; set; }
    protected VisualStudioWorkspace Workspace { get; }
 
    internal ProjectSystemProject Test_ProjectSystemProject => ProjectSystemProject;
 
    /// <summary>
    /// The path to the directory of the project. Read-only, since although you can rename
    /// a project in Visual Studio you can't change the folder of a project without an
    /// unload/reload.
    /// </summary>
    private readonly string _projectDirectory = null;
 
    /// <summary>
    /// Whether we should ignore the output path for this project because it's a special project.
    /// </summary>
    private readonly bool _ignoreOutputPath;
 
    private static readonly char[] PathSeparatorCharacters = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
 
    #region Mutable fields that should only be used from the UI thread
 
    private readonly SolutionEventsBatchScopeCreator _batchScopeCreator;
 
    #endregion
 
    public AbstractLegacyProject(
        string projectSystemName,
        IVsHierarchy hierarchy,
        string language,
        bool isVsIntellisenseProject,
        IServiceProvider serviceProvider,
        IThreadingContext threadingContext,
        string externalErrorReportingPrefix)
    {
        ThreadingContext = threadingContext;
        ThreadingContext.ThrowIfNotOnUIThread();
        Contract.ThrowIfNull(hierarchy);
 
        var componentModel = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel));
        Workspace = componentModel.GetService<VisualStudioWorkspace>();
        var workspaceImpl = (VisualStudioWorkspaceImpl)Workspace;
 
        var projectFilePath = hierarchy.TryGetProjectFilePath();
 
        if (projectFilePath != null && !File.Exists(projectFilePath))
        {
            projectFilePath = null;
        }
 
        if (projectFilePath != null)
        {
            _projectDirectory = Path.GetDirectoryName(projectFilePath);
        }
 
        if (isVsIntellisenseProject)
        {
            // IVsIntellisenseProjects are usually used for contained language cases, which means these projects don't have any real
            // output path that we should consider. Since those point to the same IVsHierarchy as another project, we end up with two projects
            // with the same output path, which potentially breaks conversion of metadata references to project references. However they're
            // also used for database projects and a few other cases where there there isn't a "primary" IVsHierarchy.
            // As a heuristic here we'll ignore the output path if we already have another project tied to the IVsHierarchy.
            foreach (var projectId in Workspace.CurrentSolution.ProjectIds)
            {
                if (Workspace.GetHierarchy(projectId) == hierarchy)
                {
                    _ignoreOutputPath = true;
                    break;
                }
            }
        }
 
        var projectFactory = componentModel.GetService<VisualStudioProjectFactory>();
        ProjectSystemProject = threadingContext.JoinableTaskFactory.Run(() => projectFactory.CreateAndAddToWorkspaceAsync(
            projectSystemName,
            language,
            new VisualStudioProjectCreationInfo
            {
                // The workspace requires an assembly name so we can make compilations. We'll use
                // projectSystemName because they'll have a better one eventually.
                AssemblyName = projectSystemName,
                FilePath = projectFilePath,
                Hierarchy = hierarchy,
                ProjectGuid = GetProjectIDGuid(hierarchy),
            },
            CancellationToken.None));
 
        workspaceImpl.AddProjectRuleSetFileToInternalMaps(
            ProjectSystemProject,
            () => ProjectSystemProjectOptionsProcessor.EffectiveRuleSetFilePath);
 
        // 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)
        ProjectSystemProject.DefaultNamespace = GetRootNamespacePropertyValue(hierarchy);
 
        if (TryGetPropertyValue(hierarchy, BuildPropertyNames.MaxSupportedLangVersion, out var maxLangVer))
        {
            ProjectSystemProject.MaxLangVersion = maxLangVer;
        }
 
        if (TryGetBoolPropertyValue(hierarchy, BuildPropertyNames.RunAnalyzers, out var runAnayzers))
        {
            ProjectSystemProject.RunAnalyzers = runAnayzers;
        }
 
        if (TryGetBoolPropertyValue(hierarchy, BuildPropertyNames.RunAnalyzersDuringLiveAnalysis, out var runAnayzersDuringLiveAnalysis))
        {
            ProjectSystemProject.RunAnalyzersDuringLiveAnalysis = runAnayzersDuringLiveAnalysis;
        }
 
        Hierarchy = hierarchy;
        ConnectHierarchyEvents();
        RefreshBinOutputPath();
 
        var projectHierarchyGuid = GetProjectIDGuid(hierarchy);
 
        _externalErrorReporter = new ProjectExternalErrorReporter(ProjectSystemProject.Id, projectHierarchyGuid, externalErrorReportingPrefix, language, workspaceImpl);
        _batchScopeCreator = componentModel.GetService<SolutionEventsBatchScopeCreator>();
        _batchScopeCreator.StartTrackingProject(ProjectSystemProject, Hierarchy);
    }
 
    public string AssemblyName => ProjectSystemProject.AssemblyName;
 
    public string GetOutputFileName()
        => ProjectSystemProject.CompilationOutputAssemblyFilePath;
 
    public virtual void Disconnect()
    {
        _batchScopeCreator.StopTrackingProject(ProjectSystemProject);
 
        ProjectSystemProjectOptionsProcessor?.Dispose();
        ProjectCodeModel.OnProjectClosed();
        ProjectSystemProject.RemoveFromWorkspace();
 
        // Unsubscribe IVsHierarchyEvents
        DisconnectHierarchyEvents();
    }
 
    protected void AddFile(
        string filename,
        SourceCodeKind sourceCodeKind)
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        // We have tests that assert that XOML files should not get added; this was similar
        // behavior to how ASP.NET projects would add .aspx files even though we ultimately ignored
        // them. XOML support is planned to go away for Dev16, but for now leave the logic there.
        if (filename.EndsWith(".xoml"))
        {
            return;
        }
 
        var folders = GetFolderNamesForDocument(filename);
 
        ProjectSystemProject.AddSourceFile(filename, sourceCodeKind, folders);
    }
 
    protected void AddFile(
        string filename,
        string linkMetadata,
        SourceCodeKind sourceCodeKind)
    {
        // We have tests that assert that XOML files should not get added; this was similar
        // behavior to how ASP.NET projects would add .aspx files even though we ultimately ignored
        // them. XOML support is planned to go away for Dev16, but for now leave the logic there.
        if (filename.EndsWith(".xoml"))
        {
            return;
        }
 
        var folders = ImmutableArray<string>.Empty;
        if (!string.IsNullOrEmpty(linkMetadata))
        {
            var linkFolderPath = Path.GetDirectoryName(linkMetadata);
            folders = linkFolderPath.Split(PathSeparatorCharacters, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray();
        }
        else if (!string.IsNullOrEmpty(ProjectSystemProject.FilePath))
        {
            var relativePath = PathUtilities.GetRelativePath(_projectDirectory, filename);
            var relativePathParts = relativePath.Split(PathSeparatorCharacters);
            folders = ImmutableArray.Create(relativePathParts, start: 0, length: relativePathParts.Length - 1);
        }
 
        ProjectSystemProject.AddSourceFile(filename, sourceCodeKind, folders);
    }
 
    protected void RemoveFile(string filename)
    {
        // We have tests that assert that XOML files should not get added; this was similar
        // behavior to how ASP.NET projects would add .aspx files even though we ultimately ignored
        // them. XOML support is planned to go away for Dev16, but for now leave the logic there.
        if (filename.EndsWith(".xoml"))
        {
            return;
        }
 
        ProjectSystemProject.RemoveSourceFile(filename);
        ProjectCodeModel.OnSourceFileRemoved(filename);
    }
 
    protected void RefreshBinOutputPath()
    {
        // These projects are created against the same hierarchy as the "main" project that
        // hosts the rest of the code; if we query the IVsHierarchy for the output path
        // we'll end up with duplicate output paths which can break P2P referencing. Since the output
        // path doesn't make sense for these, we'll ignore them.
        if (_ignoreOutputPath)
        {
            return;
        }
 
        if (Hierarchy is not IVsBuildPropertyStorage storage)
        {
            return;
        }
 
        if (ErrorHandler.Failed(storage.GetPropertyValue("OutDir", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out var outputDirectory)) ||
            ErrorHandler.Failed(storage.GetPropertyValue("TargetFileName", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out var targetFileName)))
        {
            return;
        }
 
        if (targetFileName == null)
        {
            return;
        }
 
        // web app case
        if (!PathUtilities.IsAbsolute(outputDirectory))
        {
            if (ProjectSystemProject.FilePath == null)
            {
                return;
            }
 
            outputDirectory = FileUtilities.ResolveRelativePath(outputDirectory, Path.GetDirectoryName(ProjectSystemProject.FilePath));
        }
 
        if (outputDirectory == null)
        {
            return;
        }
 
        ProjectSystemProject.OutputFilePath = FileUtilities.NormalizeAbsolutePath(Path.Combine(outputDirectory, targetFileName));
 
        if (ErrorHandler.Succeeded(storage.GetPropertyValue("TargetRefPath", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out var targetRefPath)) && !string.IsNullOrEmpty(targetRefPath))
        {
            ProjectSystemProject.OutputRefFilePath = targetRefPath;
        }
        else
        {
            ProjectSystemProject.OutputRefFilePath = null;
        }
    }
 
    private static Guid GetProjectIDGuid(IVsHierarchy hierarchy)
    {
        if (hierarchy.TryGetGuidProperty(__VSHPROPID.VSHPROPID_ProjectIDGuid, out var guid))
        {
            return guid;
        }
 
        return Guid.Empty;
    }
 
    /// <summary>
    /// Map of folder item IDs in the workspace to the string version of their path.
    /// </summary>
    /// <remarks>Using item IDs as a key like this in a long-lived way is considered unsupported by CPS and other
    /// IVsHierarchy providers, but this code (which is fairly old) still makes the assumptions anyways.</remarks>
    private readonly Dictionary<uint, ImmutableArray<string>> _folderNameMap = [];
    protected readonly IThreadingContext ThreadingContext;
 
    private ImmutableArray<string> GetFolderNamesForDocument(string filename)
    {
        var itemid = Hierarchy.TryGetItemId(filename);
        if (itemid != VSConstants.VSITEMID_NIL)
        {
            return GetFolderNamesForDocument(itemid);
        }
 
        return default;
    }
 
    private ImmutableArray<string> GetFolderNamesForDocument(uint documentItemID)
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        if (documentItemID != (uint)VSConstants.VSITEMID.Nil && Hierarchy.GetProperty(documentItemID, (int)VsHierarchyPropID.Parent, out var parentObj) == VSConstants.S_OK)
        {
            var parentID = UnboxVSItemId(parentObj);
            if (parentID is not ((uint)VSConstants.VSITEMID.Nil) and not ((uint)VSConstants.VSITEMID.Root))
            {
                return GetFolderNamesForFolder(parentID);
            }
        }
 
        return ImmutableArray<string>.Empty;
    }
 
    private ImmutableArray<string> GetFolderNamesForFolder(uint folderItemID)
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        using var pooledObject = SharedPools.Default<List<string>>().GetPooledObject();
 
        var newFolderNames = pooledObject.Object;
 
        if (!_folderNameMap.TryGetValue(folderItemID, out var folderNames))
        {
            ComputeFolderNames(folderItemID, newFolderNames, Hierarchy);
            folderNames = newFolderNames.ToImmutableArray();
            _folderNameMap.Add(folderItemID, folderNames);
        }
        else
        {
            // verify names, and change map if we get a different set.
            // this is necessary because we only get document adds/removes from the project system
            // when a document name or folder name changes.
            ComputeFolderNames(folderItemID, newFolderNames, Hierarchy);
            if (!Enumerable.SequenceEqual(folderNames, newFolderNames))
            {
                folderNames = newFolderNames.ToImmutableArray();
                _folderNameMap[folderItemID] = folderNames;
            }
        }
 
        return folderNames;
    }
 
    // Different hierarchies are inconsistent on whether they return ints or uints for VSItemIds.
    // Technically it should be a uint.  However, there's no enforcement of this, and marshalling
    // from native to managed can end up resulting in boxed ints instead.  Handle both here so 
    // we're resilient to however the IVsHierarchy was actually implemented.
    private static uint UnboxVSItemId(object id)
        => id is uint ? (uint)id : unchecked((uint)(int)id);
 
    private static void ComputeFolderNames(uint folderItemID, List<string> names, IVsHierarchy hierarchy)
    {
        if (hierarchy.GetProperty(folderItemID, (int)VsHierarchyPropID.Name, out var nameObj) == VSConstants.S_OK)
        {
            // For 'Shared' projects, IVSHierarchy returns a hierarchy item with < character in its name (i.e. <SharedProjectName>)
            // as a child of the root item. There is no such item in the 'visual' hierarchy in solution explorer and no such folder
            // is present on disk either. Since this is not a real 'folder', we exclude it from the contents of Document.Folders.
            // Note: The parent of the hierarchy item that contains < character in its name is VSITEMID.Root. So we don't need to
            // worry about accidental propagation out of the Shared project to any containing 'Solution' folders - the check for
            // VSITEMID.Root below already takes care of that.
            var name = (string)nameObj;
            if (!name.StartsWith("<", StringComparison.OrdinalIgnoreCase))
            {
                names.Insert(0, name);
            }
        }
 
        if (hierarchy.GetProperty(folderItemID, (int)VsHierarchyPropID.Parent, out var parentObj) == VSConstants.S_OK)
        {
            var parentID = UnboxVSItemId(parentObj);
            if (parentID is not ((uint)VSConstants.VSITEMID.Nil) and not ((uint)VSConstants.VSITEMID.Root))
            {
                ComputeFolderNames(parentID, names, hierarchy);
            }
        }
    }
 
    /// <summary>
    /// Get the value of "rootnamespace" property of the project ("" if not defined, which means global namespace),
    /// or null if it is unknown or not applicable. 
    /// </summary>
    /// <remarks>
    /// This property has different meaning between C# and VB, each project type can decide how to interpret the value.
    /// </remarks>>
    private static string GetRootNamespacePropertyValue(IVsHierarchy hierarchy)
    {
        // While both csproj and vbproj might define <rootnamespace> property in the project file, 
        // they are very different things.
        // 
        // In C#, it's called default namespace (even though we got the value from rootnamespace property),
        // and it doesn't affect the semantic of the code in anyway, just something used by VS.
        // For example, when you create a new class, the namespace for the new class is based on it. 
        // Therefore, we can't get this info from compiler.
        // 
        // However, in VB, it's actually called root namespace, and that info is part of the VB compilation 
        // (parsed from arguments), because VB compiler needs it to determine the root of all the namespace 
        // declared in the compilation.
        // 
        // Unfortunately, although being different concepts, default namespace and root namespace are almost
        // used interchangeably in VS. For example, (1) the value is define in "rootnamespace" property in project 
        // files and, (2) the property name we use to call into hierarchy below to retrieve the value is 
        // called "DefaultNamespace".
 
        if (hierarchy.TryGetProperty(__VSHPROPID.VSHPROPID_DefaultNamespace, out string value))
        {
            return value;
        }
 
        return null;
    }
 
    private static bool TryGetPropertyValue(IVsHierarchy hierarchy, string propertyName, out string propertyValue)
    {
        if (hierarchy is not IVsBuildPropertyStorage storage)
        {
            propertyValue = null;
            return false;
        }
 
        return ErrorHandler.Succeeded(storage.GetPropertyValue(propertyName, null, (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue));
    }
 
    private static bool TryGetBoolPropertyValue(IVsHierarchy hierarchy, string propertyName, out bool? propertyValue)
    {
        if (!TryGetPropertyValue(hierarchy, propertyName, out var stringPropertyValue))
        {
            propertyValue = null;
            return false;
        }
 
        propertyValue = bool.TryParse(stringPropertyValue, out var parsedBoolValue) ? parsedBoolValue : null;
        return true;
    }
}