File: MSBuild\ProjectFile\ProjectInstanceReader.cs
Web Access
Project: src\src\Workspaces\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 Roslyn.Utilities;
using MSB = Microsoft.Build;
 
namespace Microsoft.CodeAnalysis.MSBuild;
 
internal readonly struct ProjectInstanceReader
{
    private readonly ProjectCommandLineProvider _commandLineProvider;
    public readonly MSB.Evaluation.Project? Project;
    public readonly MSB.Execution.ProjectInstance _projectInstance;
 
    private readonly string _projectDirectory;
 
    public ProjectInstanceReader(
        ProjectCommandLineProvider commandLineReader,
        MSB.Execution.ProjectInstance projectInstance,
        MSB.Evaluation.Project? project)
    {
        _commandLineProvider = commandLineReader;
        _projectInstance = projectInstance;
        Project = project;
        _projectDirectory = PathUtilities.EnsureTrailingSeparator(PathUtilities.GetDirectoryName(_projectInstance.FullPath));
    }
 
    public string FilePath
        => _projectInstance.FullPath;
 
    public string Language
        => _commandLineProvider.Language;
 
    public ProjectFileInfo CreateProjectFileInfo()
    {
        var commandLineArgs = GetCommandLineArgs(_projectInstance);
 
        var outputFilePath = _projectInstance.ReadPropertyString(PropertyNames.TargetPath);
        if (!RoslynString.IsNullOrWhiteSpace(outputFilePath))
        {
            outputFilePath = GetAbsolutePathRelativeToProject(outputFilePath);
        }
 
        var outputRefFilePath = _projectInstance.ReadPropertyString(PropertyNames.TargetRefPath);
        if (!RoslynString.IsNullOrWhiteSpace(outputRefFilePath))
        {
            outputRefFilePath = GetAbsolutePathRelativeToProject(outputRefFilePath);
        }
 
        var generatedFilesOutputDirectory = _projectInstance.ReadPropertyString(PropertyNames.CompilerGeneratedFilesOutputPath);
        generatedFilesOutputDirectory = RoslynString.IsNullOrWhiteSpace(generatedFilesOutputDirectory)
            ? null
            : GetAbsolutePathRelativeToProject(generatedFilesOutputDirectory);
 
        var intermediateOutputFilePath = _projectInstance.GetItems(ItemNames.IntermediateAssembly).FirstOrDefault()?.EvaluatedInclude;
        if (!RoslynString.IsNullOrWhiteSpace(intermediateOutputFilePath))
        {
            intermediateOutputFilePath = GetAbsolutePathRelativeToProject(intermediateOutputFilePath);
        }
 
        var projectAssetsFilePath = _projectInstance.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 = _projectInstance.ReadPropertyString(PropertyNames.RootNamespace) ?? string.Empty;
 
        var targetFramework = _projectInstance.ReadPropertyString(PropertyNames.TargetFramework);
        if (RoslynString.IsNullOrWhiteSpace(targetFramework))
        {
            targetFramework = null;
        }
 
        var targetFrameworkIdentifier = _projectInstance.ReadPropertyString(PropertyNames.TargetFrameworkIdentifier);
 
        var targetFrameworkVersion = _projectInstance.ReadPropertyString(PropertyNames.TargetFrameworkVersion);
 
        var docs = _projectInstance.GetDocuments().SelectAsArray(
            predicate: IsNotTemporaryGeneratedFile,
            selector: MakeDocumentFileInfo);
 
        var additionalDocs = _projectInstance.GetAdditionalFiles()
            .SelectAsArray(MakeNonSourceFileDocumentFileInfo);
 
        var analyzerConfigDocs = _projectInstance.GetEditorConfigFiles()
            .SelectAsArray(MakeNonSourceFileDocumentFileInfo);
 
        var packageReferences = _projectInstance.GetPackageReferences();
 
        var projectCapabilities = _projectInstance.GetItems(ItemNames.ProjectCapability).SelectAsArray(item => item.ToString());
        var contentFileInfo = GetContentFiles(_projectInstance);
 
        var fileGlobs = Project?.GetAllGlobs().SelectAsArray(GetFileGlobs) ?? [];
 
        return new ProjectFileInfo()
        {
            Language = Language,
            FilePath = _projectInstance.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 = [.. _projectInstance.GetProjectReferences()],
            PackageReferences = packageReferences,
            ProjectCapabilities = projectCapabilities,
            ContentFilePaths = contentFileInfo,
            FileGlobs = fileGlobs
        };
 
        static FileGlobs GetFileGlobs(MSB.Evaluation.GlobResult g)
        {
            return new FileGlobs(
                Includes: [.. g.IncludeGlobs.Select(PathUtilities.ExpandAbsolutePathWithRelativeParts)],
                Excludes: [.. g.Excludes.Select(PathUtilities.ExpandAbsolutePathWithRelativeParts)],
                Removes: [.. g.Removes.Select(PathUtilities.ExpandAbsolutePathWithRelativeParts)]);
        }
    }
 
    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 = _commandLineProvider.GetCompilerCommandLineArgs(project)
            .SelectAsArray(item => item.ItemSpec);
 
        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 = _commandLineProvider.ReadCommandLineArgs(project);
        }
 
        return commandLineArgs;
    }
 
    private 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 folders = GetRelativeFolders(documentItem);
        return new DocumentFileInfo(filePath, logicalPath, isLinked, isGenerated: false, folders);
    }
 
    private DocumentFileInfo MakeNonSourceFileDocumentFileInfo(MSB.Framework.ITaskItem documentItem)
    {
        var filePath = GetDocumentFilePath(documentItem);
        var logicalPath = GetDocumentLogicalPath(documentItem, _projectDirectory);
        var isLinked = IsDocumentLinked(documentItem);
 
        var folders = GetRelativeFolders(documentItem);
        return new DocumentFileInfo(filePath, logicalPath, isLinked, isGenerated: false, 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], StringSplitOptions.RemoveEmptyEntries).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 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);
            }
        }
    }
}