File: MSBuild\ProjectFile\ProjectFile.cs
Web Access
Project: src\src\Workspaces\Core\MSBuild.BuildHost\Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj (Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost)
// 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Globbing;
using Microsoft.Build.Globbing.Extensions;
using Roslyn.Utilities;
using MSB = Microsoft.Build;
 
namespace Microsoft.CodeAnalysis.MSBuild
{
    internal abstract class ProjectFile : IProjectFile
    {
        private readonly ProjectFileLoader _loader;
        private readonly MSB.Evaluation.Project? _loadedProject;
        private readonly ProjectBuildManager _buildManager;
        private readonly string _projectDirectory;
 
        public DiagnosticLog Log { get; }
        public virtual string FilePath => _loadedProject?.FullPath ?? string.Empty;
        public string Language => _loader.Language;
 
        protected ProjectFile(ProjectFileLoader loader, MSB.Evaluation.Project? loadedProject, ProjectBuildManager buildManager, DiagnosticLog log)
        {
            _loader = loader;
            _loadedProject = loadedProject;
            _buildManager = buildManager;
            var directory = loadedProject?.DirectoryPath ?? string.Empty;
            _projectDirectory = PathUtilities.EnsureTrailingSeparator(directory);
            Log = log;
        }
 
        public ImmutableArray<DiagnosticLogItem> GetDiagnosticLogItems() => [.. Log];
 
        protected abstract IEnumerable<MSB.Framework.ITaskItem> GetCompilerCommandLineArgs(MSB.Execution.ProjectInstance executedProject);
        protected abstract ImmutableArray<string> ReadCommandLineArgs(MSB.Execution.ProjectInstance project);
 
        /// <summary>
        /// Gets project file information asynchronously. Note that this can produce multiple
        /// instances of <see cref="ProjectFileInfo"/> if the project is multi-targeted: one for
        /// each target framework.
        /// </summary>
        public async Task<ImmutableArray<ProjectFileInfo>> GetProjectFileInfosAsync(CancellationToken cancellationToken)
        {
            if (_loadedProject is null)
            {
                return [ProjectFileInfo.CreateEmpty(Language, _loadedProject?.FullPath)];
            }
 
            var targetFrameworkValue = _loadedProject.GetPropertyValue(PropertyNames.TargetFramework);
            var targetFrameworksValue = _loadedProject.GetPropertyValue(PropertyNames.TargetFrameworks);
 
            if (RoslynString.IsNullOrEmpty(targetFrameworkValue) && !RoslynString.IsNullOrEmpty(targetFrameworksValue))
            {
                // This project has a <TargetFrameworks> property, but does not specify a <TargetFramework>.
                // In this case, we need to iterate through the <TargetFrameworks>, set <TargetFramework> with
                // each value, and build the project.
 
                var targetFrameworks = targetFrameworksValue.Split(';');
 
                if (!_loadedProject.GlobalProperties.TryGetValue(PropertyNames.TargetFramework, out var initialGlobalTargetFrameworkValue))
                    initialGlobalTargetFrameworkValue = null;
 
                var results = new FixedSizeArrayBuilder<ProjectFileInfo>(targetFrameworks.Length);
                foreach (var targetFramework in targetFrameworks)
                {
                    _loadedProject.SetGlobalProperty(PropertyNames.TargetFramework, targetFramework);
                    _loadedProject.ReevaluateIfNecessary();
 
                    var projectFileInfo = await BuildProjectFileInfoAsync(cancellationToken).ConfigureAwait(false);
 
                    results.Add(projectFileInfo);
                }
 
                if (initialGlobalTargetFrameworkValue is null)
                {
                    _loadedProject.RemoveGlobalProperty(PropertyNames.TargetFramework);
                }
                else
                {
                    _loadedProject.SetGlobalProperty(PropertyNames.TargetFramework, initialGlobalTargetFrameworkValue);
                }
 
                _loadedProject.ReevaluateIfNecessary();
 
                return results.MoveToImmutable();
            }
            else
            {
                var projectFileInfo = await BuildProjectFileInfoAsync(cancellationToken).ConfigureAwait(false);
                projectFileInfo ??= ProjectFileInfo.CreateEmpty(Language, _loadedProject?.FullPath);
                return [projectFileInfo];
            }
        }
 
        private async Task<ProjectFileInfo> BuildProjectFileInfoAsync(CancellationToken cancellationToken)
        {
            if (_loadedProject is null)
            {
                return ProjectFileInfo.CreateEmpty(Language, _loadedProject?.FullPath);
            }
 
            var project = await _buildManager.BuildProjectAsync(_loadedProject, Log, cancellationToken).ConfigureAwait(false);
 
            return project != null
                ? CreateProjectFileInfo(project)
                : ProjectFileInfo.CreateEmpty(Language, _loadedProject.FullPath);
        }
 
        private ProjectFileInfo CreateProjectFileInfo(MSB.Execution.ProjectInstance project)
        {
            var commandLineArgs = GetCommandLineArgs(project);
 
            var outputFilePath = project.ReadPropertyString(PropertyNames.TargetPath);
            if (!RoslynString.IsNullOrWhiteSpace(outputFilePath))
            {
                outputFilePath = GetAbsolutePathRelativeToProject(outputFilePath);
            }
 
            var outputRefFilePath = project.ReadPropertyString(PropertyNames.TargetRefPath);
            if (!RoslynString.IsNullOrWhiteSpace(outputRefFilePath))
            {
                outputRefFilePath = GetAbsolutePathRelativeToProject(outputRefFilePath);
            }
 
            var generatedFilesOutputDirectory = project.ReadPropertyString(PropertyNames.CompilerGeneratedFilesOutputPath);
            generatedFilesOutputDirectory = RoslynString.IsNullOrWhiteSpace(generatedFilesOutputDirectory)
                ? null
                : GetAbsolutePathRelativeToProject(generatedFilesOutputDirectory);
 
            var intermediateOutputFilePath = project.GetItems(ItemNames.IntermediateAssembly).FirstOrDefault()?.EvaluatedInclude;
            if (!RoslynString.IsNullOrWhiteSpace(intermediateOutputFilePath))
            {
                intermediateOutputFilePath = GetAbsolutePathRelativeToProject(intermediateOutputFilePath);
            }
 
            var projectAssetsFilePath = project.ReadPropertyString(PropertyNames.ProjectAssetsFile);
 
            // 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)
            var defaultNamespace = project.ReadPropertyString(PropertyNames.RootNamespace) ?? string.Empty;
 
            var targetFramework = project.ReadPropertyString(PropertyNames.TargetFramework);
            if (RoslynString.IsNullOrWhiteSpace(targetFramework))
            {
                targetFramework = null;
            }
 
            var targetFrameworkIdentifier = project.ReadPropertyString(PropertyNames.TargetFrameworkIdentifier);
 
            var targetFrameworkVersion = project.ReadPropertyString(PropertyNames.TargetFrameworkVersion);
 
            var docs = project.GetDocuments()
                .Where(IsNotTemporaryGeneratedFile)
                .Select(MakeDocumentFileInfo)
                .ToImmutableArray();
 
            var additionalDocs = project.GetAdditionalFiles()
                .Select(MakeNonSourceFileDocumentFileInfo)
                .ToImmutableArray();
 
            var analyzerConfigDocs = project.GetEditorConfigFiles()
                .Select(MakeNonSourceFileDocumentFileInfo)
                .ToImmutableArray();
 
            var packageReferences = project.GetPackageReferences();
 
            var projectCapabilities = project.GetItems(ItemNames.ProjectCapability).SelectAsArray(item => item.ToString());
            var contentFileInfo = GetContentFiles(project);
 
            var fileGlobs = _loadedProject?.GetAllGlobs().Select(GetFileGlobs).ToImmutableArray() ?? [];
 
            return new ProjectFileInfo()
            {
                Language = Language,
                FilePath = project.FullPath,
                OutputFilePath = outputFilePath,
                OutputRefFilePath = outputRefFilePath,
                GeneratedFilesOutputDirectory = generatedFilesOutputDirectory,
                IntermediateOutputFilePath = intermediateOutputFilePath,
                DefaultNamespace = defaultNamespace,
                TargetFramework = targetFramework,
                TargetFrameworkIdentifier = targetFrameworkIdentifier,
                TargetFrameworkVersion = targetFrameworkVersion,
                ProjectAssetsFilePath = projectAssetsFilePath,
                CommandLineArgs = commandLineArgs,
                Documents = docs,
                AdditionalDocuments = additionalDocs,
                AnalyzerConfigDocuments = analyzerConfigDocs,
                ProjectReferences = project.GetProjectReferences().ToImmutableArray(),
                PackageReferences = packageReferences,
                ProjectCapabilities = projectCapabilities,
                ContentFilePaths = contentFileInfo,
                FileGlobs = fileGlobs
            };
 
            static FileGlobs GetFileGlobs(GlobResult g)
            {
                return new FileGlobs(g.IncludeGlobs.ToImmutableArray(), g.Excludes.ToImmutableArray(), g.Removes.ToImmutableArray());
            }
        }
 
        private static ImmutableArray<string> GetContentFiles(MSB.Execution.ProjectInstance project)
        {
            var contentFiles = project
                .GetItems(ItemNames.Content)
                .SelectAsArray(item => item.GetMetadataValue(MetadataNames.FullPath));
            return contentFiles;
        }
 
        private ImmutableArray<string> GetCommandLineArgs(MSB.Execution.ProjectInstance project)
        {
            var commandLineArgs = GetCompilerCommandLineArgs(project)
                .Select(item => item.ItemSpec)
                .ToImmutableArray();
 
            if (commandLineArgs.Length == 0)
            {
                // We didn't get any command-line args, which likely means that the build
                // was not successful. In that case, try to read the command-line args from
                // the ProjectInstance that we have. This is a best effort to provide something
                // meaningful for the user, though it will likely be incomplete.
                commandLineArgs = ReadCommandLineArgs(project);
            }
 
            return commandLineArgs;
        }
 
        protected static bool IsNotTemporaryGeneratedFile(MSB.Framework.ITaskItem item)
            => !Path.GetFileName(item.ItemSpec).StartsWith("TemporaryGeneratedFile_", StringComparison.Ordinal);
 
        private DocumentFileInfo MakeDocumentFileInfo(MSB.Framework.ITaskItem documentItem)
        {
            var filePath = GetDocumentFilePath(documentItem);
            var logicalPath = GetDocumentLogicalPath(documentItem, _projectDirectory);
            var isLinked = IsDocumentLinked(documentItem);
            var isGenerated = IsDocumentGenerated(documentItem);
 
            var folders = GetRelativeFolders(documentItem);
            return new DocumentFileInfo(filePath, logicalPath, isLinked, isGenerated, folders);
        }
 
        private DocumentFileInfo MakeNonSourceFileDocumentFileInfo(MSB.Framework.ITaskItem documentItem)
        {
            var filePath = GetDocumentFilePath(documentItem);
            var logicalPath = GetDocumentLogicalPath(documentItem, _projectDirectory);
            var isLinked = IsDocumentLinked(documentItem);
            var isGenerated = IsDocumentGenerated(documentItem);
 
            var folders = GetRelativeFolders(documentItem);
            return new DocumentFileInfo(filePath, logicalPath, isLinked, isGenerated, folders);
        }
 
        private ImmutableArray<string> GetRelativeFolders(MSB.Framework.ITaskItem documentItem)
        {
            var linkPath = documentItem.GetMetadata(MetadataNames.Link);
            if (!RoslynString.IsNullOrEmpty(linkPath))
            {
                return [.. PathUtilities.GetDirectoryName(linkPath).Split(PathUtilities.DirectorySeparatorChar, PathUtilities.AltDirectorySeparatorChar)];
            }
            else
            {
                var filePath = documentItem.ItemSpec;
                var relativePath = PathUtilities.GetDirectoryName(PathUtilities.GetRelativePath(_projectDirectory, filePath));
                var folders = relativePath == null ? [] : relativePath.Split(PathUtilities.DirectorySeparatorChar, PathUtilities.AltDirectorySeparatorChar).ToImmutableArray();
                return folders;
            }
        }
 
        /// <summary>
        /// Resolves the given path that is possibly relative to the project directory.
        /// </summary>
        /// <remarks>
        /// The resulting path is absolute but might not be normalized.
        /// </remarks>
        private string GetAbsolutePathRelativeToProject(string path)
        {
            // TODO (tomat): should we report an error when drive-relative path (e.g. "C:goo.cs") is encountered?
            var absolutePath = FileUtilities.ResolveRelativePath(path, _projectDirectory) ?? path;
            return FileUtilities.TryNormalizeAbsolutePath(absolutePath) ?? absolutePath;
        }
 
        private string GetDocumentFilePath(MSB.Framework.ITaskItem documentItem)
            => GetAbsolutePathRelativeToProject(documentItem.ItemSpec);
 
        private static bool IsDocumentLinked(MSB.Framework.ITaskItem documentItem)
            => !RoslynString.IsNullOrEmpty(documentItem.GetMetadata(MetadataNames.Link));
 
        private IDictionary<string, MSB.Evaluation.ProjectItem>? _documents;
 
        protected bool IsDocumentGenerated(MSB.Framework.ITaskItem documentItem)
        {
            if (_documents == null)
            {
                _documents = new Dictionary<string, MSB.Evaluation.ProjectItem>();
                if (_loadedProject is null)
                {
                    return false;
                }
 
                foreach (var item in _loadedProject.GetItems(ItemNames.Compile))
                {
                    _documents[GetAbsolutePathRelativeToProject(item.EvaluatedInclude)] = item;
                }
            }
 
            return !_documents.ContainsKey(GetAbsolutePathRelativeToProject(documentItem.ItemSpec));
        }
 
        protected static string GetDocumentLogicalPath(MSB.Framework.ITaskItem documentItem, string projectDirectory)
        {
            var link = documentItem.GetMetadata(MetadataNames.Link);
            if (!RoslynString.IsNullOrEmpty(link))
            {
                // if a specific link is specified in the project file then use it to form the logical path.
                return link;
            }
            else
            {
                var filePath = documentItem.ItemSpec;
 
                if (!PathUtilities.IsAbsolute(filePath))
                {
                    return filePath;
                }
 
                var normalizedPath = FileUtilities.TryNormalizeAbsolutePath(filePath);
                if (normalizedPath == null)
                {
                    return filePath;
                }
 
                // If the document is within the current project directory (or subdirectory), then the logical path is the relative path 
                // from the project's directory.
                if (normalizedPath.StartsWith(projectDirectory, StringComparison.OrdinalIgnoreCase))
                {
                    return normalizedPath[projectDirectory.Length..];
                }
                else
                {
                    // if the document lies outside the project's directory (or subdirectory) then place it logically at the root of the project.
                    // if more than one document ends up with the same logical name then so be it (the workspace will survive.)
                    return PathUtilities.GetFileName(normalizedPath);
                }
            }
        }
 
        public void AddDocument(string filePath, string? logicalPath = null)
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, filePath);
 
            Dictionary<string, string>? metadata = null;
            if (logicalPath != null && relativePath != logicalPath)
            {
                metadata = new Dictionary<string, string>
                {
                    { MetadataNames.Link, logicalPath }
                };
 
                relativePath = filePath; // link to full path
            }
 
            _loadedProject.AddItem(ItemNames.Compile, relativePath, metadata);
        }
 
        public void RemoveDocument(string filePath)
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, filePath);
 
            var items = _loadedProject.GetItems(ItemNames.Compile);
            var item = items.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
                                               || PathUtilities.PathsEqual(it.EvaluatedInclude, filePath));
            if (item != null)
            {
                _loadedProject.RemoveItem(item);
            }
        }
 
        public void AddMetadataReference(string metadataReferenceIdentity, ImmutableArray<string> aliases, string? hintPath)
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            var metadata = new Dictionary<string, string>();
            if (!aliases.IsEmpty)
                metadata.Add(MetadataNames.Aliases, string.Join(",", aliases));
 
            if (hintPath is not null)
                metadata.Add(MetadataNames.HintPath, hintPath);
 
            _loadedProject.AddItem(ItemNames.Reference, metadataReferenceIdentity, metadata);
        }
 
        public void RemoveMetadataReference(string shortAssemblyName, string fullAssemblyName, string filePath)
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            var item = FindReferenceItem(shortAssemblyName, fullAssemblyName, filePath);
            if (item != null)
            {
                _loadedProject.RemoveItem(item);
            }
        }
 
        private MSB.Evaluation.ProjectItem FindReferenceItem(string shortAssemblyName, string fullAssemblyName, string filePath)
        {
            Contract.ThrowIfNull(_loadedProject, "The project was not loaded.");
 
            var references = _loadedProject.GetItems(ItemNames.Reference);
            MSB.Evaluation.ProjectItem? item = null;
 
            var fileName = Path.GetFileNameWithoutExtension(filePath);
 
            // check for short name match
            item = references.FirstOrDefault(it => string.Compare(it.EvaluatedInclude, shortAssemblyName, StringComparison.OrdinalIgnoreCase) == 0);
            if (item is not null)
                return item;
 
            // check for full name match
            item = references.FirstOrDefault(it => string.Compare(it.EvaluatedInclude, fullAssemblyName, StringComparison.OrdinalIgnoreCase) == 0);
            if (item is not null)
                return item;
 
            // check for file path match
            var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, filePath);
 
            item = references.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, filePath)
                                                    || PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
                                                    || PathUtilities.PathsEqual(GetHintPath(it), filePath)
                                                    || PathUtilities.PathsEqual(GetHintPath(it), relativePath));
 
            if (item is not null)
                return item;
 
            var partialName = shortAssemblyName + ",";
            var items = references.Where(it => it.EvaluatedInclude.StartsWith(partialName, StringComparison.OrdinalIgnoreCase)).ToList();
            if (items.Count == 1)
            {
                return items[0];
            }
 
            throw new InvalidOperationException($"Unable to find reference item '{shortAssemblyName}'");
        }
 
        private static string GetHintPath(MSB.Evaluation.ProjectItem item)
            => item.Metadata.FirstOrDefault(m => string.Equals(m.Name, MetadataNames.HintPath, StringComparison.OrdinalIgnoreCase))?.EvaluatedValue ?? string.Empty;
 
        public void AddProjectReference(string projectName, ProjectFileReference reference)
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            var metadata = new Dictionary<string, string>
            {
                { MetadataNames.Name, projectName }
            };
 
            if (!reference.Aliases.IsEmpty)
            {
                metadata.Add(MetadataNames.Aliases, string.Join(",", reference.Aliases));
            }
 
            var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, reference.Path);
            _loadedProject.AddItem(ItemNames.ProjectReference, relativePath, metadata);
        }
 
        public void RemoveProjectReference(string projectName, string projectFilePath)
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            var item = FindProjectReferenceItem(projectName, projectFilePath);
            if (item != null)
            {
                _loadedProject.RemoveItem(item);
            }
        }
 
        private MSB.Evaluation.ProjectItem? FindProjectReferenceItem(string projectName, string projectFilePath)
        {
            if (_loadedProject is null)
            {
                return null;
            }
 
            var references = _loadedProject.GetItems(ItemNames.ProjectReference);
            var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, projectFilePath);
 
            MSB.Evaluation.ProjectItem? item = null;
 
            // find by project file path
            item = references.First(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
                                       || PathUtilities.PathsEqual(it.EvaluatedInclude, projectFilePath));
 
            // try to find by project name
            item ??= references.First(it => string.Compare(projectName, it.GetMetadataValue(MetadataNames.Name), StringComparison.OrdinalIgnoreCase) == 0);
 
            return item;
        }
 
        public void AddAnalyzerReference(string fullPath)
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, fullPath);
            _loadedProject.AddItem(ItemNames.Analyzer, relativePath);
        }
 
        public void RemoveAnalyzerReference(string fullPath)
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, fullPath);
 
            var analyzers = _loadedProject.GetItems(ItemNames.Analyzer);
            var item = analyzers.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
                                                    || PathUtilities.PathsEqual(it.EvaluatedInclude, fullPath));
            if (item != null)
            {
                _loadedProject.RemoveItem(item);
            }
        }
 
        public void Save()
        {
            if (_loadedProject is null)
            {
                return;
            }
 
            _loadedProject.Save();
        }
    }
}