File: ResolveProjectBase.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Xml;
 
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Base class for ResolveNonMSBuildProjectOutput and AssignProjectConfiguration, since they have
    /// similar architecture
    /// </summary>
    public abstract class ResolveProjectBase : TaskExtension
    {
        #region Properties
 
        /// <summary>
        /// The list of project references
        /// </summary>
        [Required]
        public ITaskItem[] ProjectReferences
        {
            get
            {
                ErrorUtilities.VerifyThrowArgumentNull(_projectReferences, nameof(ProjectReferences));
                return _projectReferences;
            }
            set => _projectReferences = value;
        }
 
        private ITaskItem[] _projectReferences;
 
        // This field stores all the distinct project references by project absolute path
        private readonly HashSet<string> _cachedProjectReferencesByAbsolutePath = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        private SolutionConfiguration _solutionConfiguration = SolutionConfiguration.Empty;
 
        private const string attributeProject = "Project";
 
        #endregion
 
        #region Methods
 
        /// <summary>
        /// Checks if a project reference task item contains all the required attributes.
        /// Currently, the only required attribute is project GUID for inside the IDE mode.
        /// </summary>
        internal bool VerifyReferenceAttributes(ITaskItem reference, out string missingAttribute)
        {
            missingAttribute = attributeProject;
            string attrValue = reference.GetMetadata(missingAttribute);
 
            // missing project GUID? (no longer required, but if it's there then validate it)
            if (attrValue.Length > 0)
            {
                // invalid project GUID format?
                if (!Guid.TryParse(attrValue, out _))
                {
                    return false;
                }
            }
 
            missingAttribute = null;
 
            return true;
        }
 
        /// <summary>
        /// Checks all project reference task items for required attributes
        /// Internal for unit testing
        /// </summary>
        internal bool VerifyProjectReferenceItems(ITaskItem[] references, bool treatAsError)
        {
            bool referencesValid = true;
 
            foreach (ITaskItem reference in references)
            {
                _cachedProjectReferencesByAbsolutePath.Add(reference.GetMetadata("FullPath")); // metadata is cached and used again later
 
                if (!VerifyReferenceAttributes(reference, out string missingAttribute))
                {
                    if (treatAsError)
                    {
                        Log.LogErrorWithCodeFromResources("General.MissingOrUnknownProjectReferenceAttribute", reference.ItemSpec, missingAttribute);
                        referencesValid = false;
                    }
                    else
                    {
                        Log.LogWarningWithCodeFromResources("General.MissingOrUnknownProjectReferenceAttribute", reference.ItemSpec, missingAttribute);
                    }
                }
            }
 
            return referencesValid;
        }
 
        /// <summary>
        /// Pre-cache individual project elements from the XML string in a hashtable for quicker access.
        /// </summary>
        internal void CacheProjectElementsFromXml(string xmlString) => _solutionConfiguration = new SolutionConfiguration(xmlString);
 
        /// <summary>
        /// Helper method for retrieving whatever was stored in the XML string for the given project
        /// </summary>
        protected string GetProjectItem(ITaskItem projectRef)
        {
            XmlElement projectElement = GetProjectElement(projectRef);
            return projectElement?.InnerText;
        }
 
        /// <summary>
        /// Helper method for retrieving the XML element for the given project
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1059:MembersShouldNotExposeCertainConcreteTypes", MessageId = "System.Xml.XmlNode", Justification = "protected method on a public base class that has previously shipped, so changing this now would be a breaking change.")]
        protected XmlElement GetProjectElement(ITaskItem projectRef)
        {
            string projectGuid = projectRef.GetMetadata(attributeProject);
 
            if (_solutionConfiguration.TryGetProjectByGuid(projectGuid, out XmlElement projectElement))
            {
                return projectElement;
            }
 
            // We didn't find the project element by locating a project guid on the P2P reference
            // next we'll try a lookup by the absolute path of the project
            string projectFullPath = projectRef.GetMetadata("FullPath"); // reserved metadata "FullPath" is used at it will cache the value
 
            if (_solutionConfiguration.TryGetProjectByAbsolutePath(projectFullPath, out projectElement))
            {
                return projectElement;
            }
 
            return null;
        }
 
        /// <summary>
        /// Helper method for retrieving the extra "project references" passed in the solution blob.
        /// These came from dependencies expressed in the solution file itself.
        /// </summary>
        protected void AddSyntheticProjectReferences(string currentProjectAbsolutePath)
        {
            // Get the guid for this project
            if (!_solutionConfiguration.TryGetProjectGuidByAbsolutePath(currentProjectAbsolutePath, out string projectGuid))
            {
                // We were passed a blob, but we weren't listed in it. Odd. Return.
                return;
            }
 
            // Use the guid to look up the dependencies for it
            if (!_solutionConfiguration.TryGetProjectDependencies(projectGuid, out List<string> guids))
            {
                // We didn't have dependencies listed in the blob
                return;
            }
 
            // ProjectReferences is a fixed size array, so start aggregating in a list
            List<ITaskItem> updatedProjectReferenceList = new List<ITaskItem>(_projectReferences);
 
            foreach (string guid in guids)
            {
                // Get the absolute path of the dependency, using the blob
                if (!_solutionConfiguration.TryGetProjectPathByGuid(guid, out string path))
                {
                    // We had a dependency listed in the blob that wasn't itself in the blob. Odd. Return.
                    continue;
                }
 
                // If the dependency's already specified as a project reference, ignore it; no sense referencing it twice
                if (!_cachedProjectReferencesByAbsolutePath.Contains(path))
                {
                    _cachedProjectReferencesByAbsolutePath.Add(path);
                    var item = new TaskItem(path);
 
                    // Unfortunately we've used several different metadata names to trigger
                    // project references to do stuff other than trigger a build
                    item.SetMetadata("ReferenceOutputAssembly", "false");
                    item.SetMetadata("LinkLibraryDependencies", "false");
                    item.SetMetadata("CopyLocal", "false");
                    item.SetMetadata("SkipGetTargetFrameworkProperties", "true");
                    item.SetMetadata("GlobalPropertiesToRemove", "TargetFramework");
 
                    updatedProjectReferenceList.Add(item);
                }
            }
 
            // Finally, set our new augmented project references list as the official list;
            // note that this means that the output parameter may include project references that weren't passed in
            _projectReferences = new ITaskItem[updatedProjectReferenceList.Count];
            updatedProjectReferenceList.CopyTo(_projectReferences);
        }
 
        #endregion
    }
}