File: Graph\ProjectInterpretation.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// 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.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Shared;
 
#nullable disable
 
namespace Microsoft.Build.Graph
{
    internal sealed class ProjectInterpretation
    {
        private const string FullPathMetadataName = "FullPath";
        private const string ToolsVersionMetadataName = "ToolsVersion";
        private const string SetConfigurationMetadataName = "SetConfiguration";
        private const string SetPlatformMetadataName = "SetPlatform";
        private const string SetTargetFrameworkMetadataName = "SetTargetFramework";
        private const string GlobalPropertiesToRemoveMetadataName = "GlobalPropertiesToRemove";
        private const string ProjectReferenceTargetIsOuterBuildMetadataName = "OuterBuild";
        private const string InnerBuildReferenceItemName = "_ProjectSelfReference";
        internal static string TransitiveReferenceItemName = "_TransitiveProjectReference";
        internal const string AddTransitiveProjectReferencesInStaticGraphPropertyName = "AddTransitiveProjectReferencesInStaticGraph";
        private const string PlatformLookupTableMetadataName = "PlatformLookupTable";
        private const string PlatformMetadataName = "Platform";
        private const string PlatformsMetadataName = "Platforms";
        private const string EnableDynamicPlatformResolutionPropertyName = "EnableDynamicPlatformResolution";
        private const string OverridePlatformNegotiationValue = "OverridePlatformNegotiationValue";
        private const string ShouldUnsetParentConfigurationAndPlatformPropertyName = "ShouldUnsetParentConfigurationAndPlatform";
        private const string ProjectMetadataName = "Project";
        private const string ConfigurationMetadataName = "Configuration";
 
        private static readonly char[] PropertySeparator = MSBuildConstants.SemicolonChar;
 
        public static ProjectInterpretation Instance = new ProjectInterpretation();
 
        private static readonly ImmutableList<GlobalPropertiesModifier> ModifierForNonMultitargetingNodes = [(GlobalPropertiesModifier)ProjectReferenceGlobalPropertiesModifier];
 
        internal enum ProjectType
        {
            OuterBuild,
            InnerBuild,
            NonMultitargeting,
        }
 
        internal readonly record struct ReferenceInfo(ConfigurationMetadata ReferenceConfiguration, ProjectItemInstance ProjectReferenceItem);
 
        private readonly struct TargetSpecification
        {
            public TargetSpecification(string target, bool skipIfNonexistent)
            {
                // Verify that if this target is skippable then it equals neither
                // ".default" nor ".projectReferenceTargetsOrDefaultTargets".
                ErrorUtilities.VerifyThrow(
                    !skipIfNonexistent || (!target.Equals(MSBuildConstants.DefaultTargetsMarker)
                    && !target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker)),
                    target + " cannot be marked as SkipNonexistentTargets");
                Target = target;
                SkipIfNonexistent = skipIfNonexistent;
            }
 
            public string Target { get; }
 
            public bool SkipIfNonexistent { get; }
        }
 
        public IEnumerable<ReferenceInfo> GetReferences(ProjectGraphNode projectGraphNode, ProjectCollection projectCollection, ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory)
        {
            IEnumerable<ProjectItemInstance> projectReferenceItems;
            IEnumerable<GlobalPropertiesModifier> globalPropertiesModifiers = null;
 
            ProjectInstance requesterInstance = projectGraphNode.ProjectInstance;
 
            switch (projectGraphNode.ProjectType)
            {
                case ProjectType.OuterBuild:
                    projectReferenceItems = ConstructInnerBuildReferences(requesterInstance);
                    break;
                case ProjectType.InnerBuild:
                    globalPropertiesModifiers = ModifierForNonMultitargetingNodes.Add((parts, reference) => parts.AddPropertyToUndefine(GetInnerBuildPropertyName(requesterInstance)));
                    projectReferenceItems = requesterInstance.GetItems(ItemTypeNames.ProjectReference);
                    break;
                case ProjectType.NonMultitargeting:
                    globalPropertiesModifiers = ModifierForNonMultitargetingNodes;
                    projectReferenceItems = requesterInstance.GetItems(ItemTypeNames.ProjectReference);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
 
            SolutionConfiguration solutionConfiguration = null;
            string solutionConfigurationXml = requesterInstance.GetEngineRequiredPropertyValue(SolutionProjectGenerator.CurrentSolutionConfigurationContents);
            if (!string.IsNullOrWhiteSpace(solutionConfigurationXml))
            {
                solutionConfiguration = new SolutionConfiguration(solutionConfigurationXml);
            }
 
            foreach (ProjectItemInstance projectReferenceItem in projectReferenceItems)
            {
                if (!String.IsNullOrEmpty(projectReferenceItem.GetMetadataValue(ToolsVersionMetadataName)))
                {
                    throw new InvalidOperationException(
                        String.Format(
                            CultureInfo.InvariantCulture,
                            ResourceUtilities.GetResourceString(
                                "ProjectGraphDoesNotSupportProjectReferenceWithToolset"),
                            projectReferenceItem.EvaluatedInclude,
                            requesterInstance.FullPath));
                }
 
                string projectReferenceFullPath = projectReferenceItem.GetMetadataValue(FullPathMetadataName);
                bool enableDynamicPlatformResolution = ConversionUtilities.ValidBooleanTrue(requesterInstance.GetEngineRequiredPropertyValue(EnableDynamicPlatformResolutionPropertyName));
 
                PropertyDictionary<ProjectPropertyInstance> referenceGlobalProperties = GetGlobalPropertiesForItem(
                    projectReferenceItem,
                    requesterInstance.GlobalPropertiesDictionary,
                    // Only allow reuse in scenarios where we will not mutate the collection.
                    // TODO: Should these mutations be moved to globalPropertiesModifiers in the future?
                    allowCollectionReuse: solutionConfiguration == null && !enableDynamicPlatformResolution,
                    globalPropertiesModifiers);
 
                bool configurationDefined = false;
 
                // Match what AssignProjectConfiguration does to resolve project references.
                if (solutionConfiguration != null)
                {
                    string projectGuid = projectReferenceItem.GetMetadataValue(ProjectMetadataName);
                    if (solutionConfiguration.TryGetProjectByGuid(projectGuid, out XmlElement projectElement)
                        || solutionConfiguration.TryGetProjectByAbsolutePath(projectReferenceFullPath, out projectElement))
                    {
                        // Note: AssignProjectConfiguration sets various metadata on the ProjectReference item, but ultimately it just translates to the Configuration and Platform global properties on the MSBuild task.
                        string projectConfiguration = projectElement.InnerText;
                        string[] configurationPlatformParts = projectConfiguration.Split(SolutionConfiguration.ConfigPlatformSeparator[0]);
                        SetProperty(referenceGlobalProperties, ConfigurationMetadataName, configurationPlatformParts[0]);
 
                        if (configurationPlatformParts.Length > 1)
                        {
                            SetProperty(referenceGlobalProperties, PlatformMetadataName, configurationPlatformParts[1]);
                        }
                        else
                        {
                            referenceGlobalProperties.Remove(PlatformMetadataName);
                        }
 
                        configurationDefined = true;
                    }
                    else
                    {
                        // Note: ShouldUnsetParentConfigurationAndPlatform defaults to true in the AssignProjectConfiguration target when building a solution, so check that it's not false instead of checking that it's true.
                        bool shouldUnsetParentConfigurationAndPlatform = !ConversionUtilities.ValidBooleanFalse(requesterInstance.GetEngineRequiredPropertyValue(ShouldUnsetParentConfigurationAndPlatformPropertyName));
                        if (shouldUnsetParentConfigurationAndPlatform)
                        {
                            referenceGlobalProperties.Remove(ConfigurationMetadataName);
                            referenceGlobalProperties.Remove(PlatformMetadataName);
                        }
                        else
                        {
                            configurationDefined = true;
                        }
                    }
                }
 
                // Note: Dynamic platform resolution is not enabled for sln-based builds,
                // unless the project isn't known to the solution.
                if (enableDynamicPlatformResolution && !configurationDefined && !projectReferenceItem.HasMetadata(SetPlatformMetadataName))
                {
                    string requesterPlatform = requesterInstance.GetEngineRequiredPropertyValue("Platform");
                    string requesterPlatformLookupTable = requesterInstance.GetEngineRequiredPropertyValue("PlatformLookupTable");
 
                    var projectInstance = projectInstanceFactory(
                        projectReferenceFullPath,
                        null, // Platform negotiation requires an evaluation with no global properties first
                        projectCollection);
 
                    string overridePlatformNegotiationMetadataValue = projectReferenceItem.GetMetadataValue(OverridePlatformNegotiationValue);
 
                    var selectedPlatform = PlatformNegotiation.GetNearestPlatform(overridePlatformNegotiationMetadataValue, projectInstance.GetEngineRequiredPropertyValue(PlatformMetadataName), projectInstance.GetEngineRequiredPropertyValue(PlatformsMetadataName), projectInstance.GetEngineRequiredPropertyValue(PlatformLookupTableMetadataName), requesterInstance.GetEngineRequiredPropertyValue(PlatformLookupTableMetadataName), projectInstance.FullPath, requesterInstance.GetEngineRequiredPropertyValue(PlatformMetadataName));
 
                    if (selectedPlatform.Equals(String.Empty))
                    {
                        referenceGlobalProperties.Remove(PlatformMetadataName);
                    }
                    else
                    {
                        SetProperty(referenceGlobalProperties, PlatformMetadataName, selectedPlatform);
                    }
                }
 
                var referenceConfig = new ConfigurationMetadata(projectReferenceFullPath, referenceGlobalProperties);
 
                yield return new ReferenceInfo(referenceConfig, projectReferenceItem);
 
                static void SetProperty(PropertyDictionary<ProjectPropertyInstance> properties, string propertyName, string propertyValue)
                {
                    ProjectPropertyInstance propertyInstance = ProjectPropertyInstance.Create(propertyName, propertyValue);
                    properties[propertyName] = propertyInstance;
                }
            }
        }
 
        internal static string GetInnerBuildPropertyValue(ProjectInstance project)
        {
            return project.GetPropertyValue(GetInnerBuildPropertyName(project));
        }
 
        internal static string GetInnerBuildPropertyName(ProjectInstance project)
        {
            return project.GetPropertyValue(PropertyNames.InnerBuildProperty);
        }
 
        internal static string GetInnerBuildPropertyValues(ProjectInstance project)
        {
            return project.GetPropertyValue(project.GetPropertyValue(PropertyNames.InnerBuildPropertyValues));
        }
 
        internal static ProjectType GetProjectType(ProjectInstance project)
        {
            var isOuterBuild = String.IsNullOrWhiteSpace(GetInnerBuildPropertyValue(project)) && !String.IsNullOrWhiteSpace(GetInnerBuildPropertyValues(project));
            var isInnerBuild = !String.IsNullOrWhiteSpace(GetInnerBuildPropertyValue(project));
 
            ErrorUtilities.VerifyThrow(!(isOuterBuild && isInnerBuild), $"A project cannot be an outer and inner build at the same time: ${project.FullPath}");
 
            return isOuterBuild
                ? ProjectType.OuterBuild
                : isInnerBuild
                    ? ProjectType.InnerBuild
                    : ProjectType.NonMultitargeting;
        }
 
        /// <summary>
        /// To avoid calling nuget at graph construction time, the graph is initially constructed with nodes referencing outer build nodes which in turn
        /// reference inner build nodes. However at build time, the inner builds are referenced directly by the nodes referencing the outer build.
        /// Change the graph to mimic this behaviour.
        /// Example: Node -> Outer -> Inner go to: Node -> Outer; Node->Inner; Outer -> Inner. Inner build edges get added to Node.
        /// </summary>
        public void AddInnerBuildEdges(Dictionary<ConfigurationMetadata, ParsedProject> allNodes, GraphBuilder graphBuilder)
        {
            foreach (KeyValuePair<ConfigurationMetadata, ParsedProject> node in allNodes)
            {
                ProjectGraphNode outerBuild = node.Value.GraphNode;
 
                if (outerBuild.ProjectType == ProjectType.OuterBuild && outerBuild.ReferencingProjects.Count != 0)
                {
                    foreach (ProjectGraphNode innerBuild in outerBuild.ProjectReferences)
                    {
                        foreach (ProjectGraphNode outerBuildReferencingProject in outerBuild.ReferencingProjects)
                        {
                            // Which edge should be used to connect the outerBuildReferencingProject to the inner builds?
                            // Decided to use the outerBuildBuildReferencingProject -> outerBuild edge in order to preserve any extra metadata
                            // information that may be present on the edge, like the "Targets" metadata which specifies what
                            // targets to call on the references.
                            ProjectItemInstance newInnerBuildEdge = graphBuilder.Edges[(outerBuildReferencingProject, outerBuild)];
 
                            if (outerBuildReferencingProject.ProjectReferences.Contains(innerBuild))
                            {
                                ErrorUtilities.VerifyThrow(
                                    graphBuilder.Edges[(outerBuildReferencingProject, innerBuild)]
                                        .ItemType.Equals(
                                            TransitiveReferenceItemName,
                                            StringComparison.OrdinalIgnoreCase),
                                    "Only transitive references may reference inner builds that got generated by outer builds");
 
                                outerBuildReferencingProject.RemoveReference(innerBuild, graphBuilder.Edges);
                            }
 
                            outerBuildReferencingProject.AddProjectReference(innerBuild, newInnerBuildEdge, graphBuilder.Edges);
                        }
                    }
                }
            }
        }
 
        private static IEnumerable<ProjectItemInstance> ConstructInnerBuildReferences(ProjectInstance outerBuild)
        {
            var globalPropertyName = GetInnerBuildPropertyName(outerBuild);
            var globalPropertyValues = GetInnerBuildPropertyValues(outerBuild);
 
            ErrorUtilities.VerifyThrow(!String.IsNullOrWhiteSpace(globalPropertyName), "Must have an inner build property");
            ErrorUtilities.VerifyThrow(!String.IsNullOrWhiteSpace(globalPropertyValues), "Must have values for the inner build property");
 
            foreach (var globalPropertyValue in ExpressionShredder.SplitSemiColonSeparatedList(globalPropertyValues))
            {
                yield return new ProjectItemInstance(
                    project: outerBuild,
                    itemType: InnerBuildReferenceItemName,
                    includeEscaped: outerBuild.FullPath,
                    directMetadata: [new KeyValuePair<string, string>(ItemMetadataNames.PropertiesMetadataName, $"{globalPropertyName}={globalPropertyValue}")],
                    definingFileEscaped: outerBuild.FullPath);
            }
        }
 
        /// <summary>
        ///     Gets the effective global properties for a project reference item.
        /// </summary>
        /// <remarks>
        ///     The behavior of this method should match the logic in the SDK
        /// </remarks>
        private static GlobalPropertyPartsForMSBuildTask ProjectReferenceGlobalPropertiesModifier(
            GlobalPropertyPartsForMSBuildTask defaultParts,
            ProjectItemInstance projectReference)
        {
            // ProjectReference defines yet another metadata name containing properties to undefine. Merge it in if non empty.
            var globalPropertiesToRemove = SplitPropertyNames(projectReference.GetMetadataValue(GlobalPropertiesToRemoveMetadataName));
 
            var newUndefineProperties = defaultParts.UndefineProperties;
 
            newUndefineProperties = newUndefineProperties.AddRange(defaultParts.UndefineProperties);
            newUndefineProperties = newUndefineProperties.AddRange(globalPropertiesToRemove);
 
            newUndefineProperties.Add("InnerBuildProperty");
 
            var newProperties = defaultParts.Properties;
 
            // The properties on the project reference supersede the ones from the MSBuild task instead of appending.
            if (newProperties.Count == 0)
            {
                // TODO: Mimic AssignProjectConfiguration's behavior for determining the values for these.
                var setConfigurationString = projectReference.GetMetadataValue(SetConfigurationMetadataName);
                var setPlatformString = projectReference.GetMetadataValue(SetPlatformMetadataName);
                var setTargetFrameworkString = projectReference.GetMetadataValue(SetTargetFrameworkMetadataName);
 
                if (!String.IsNullOrEmpty(setConfigurationString) || !String.IsNullOrEmpty(setPlatformString) || !String.IsNullOrEmpty(setTargetFrameworkString))
                {
                    newProperties = SplitPropertyNameValuePairs(
                        ItemMetadataNames.PropertiesMetadataName,
                        $"{setConfigurationString};{setPlatformString};{setTargetFrameworkString}").ToImmutableDictionary();
                }
            }
 
            return new GlobalPropertyPartsForMSBuildTask(newProperties, defaultParts.AdditionalProperties, newUndefineProperties);
        }
 
        private readonly struct GlobalPropertyPartsForMSBuildTask
        {
            public ImmutableDictionary<string, string> Properties { get; }
            public ImmutableDictionary<string, string> AdditionalProperties { get; }
            public ImmutableList<string> UndefineProperties { get; }
 
            public GlobalPropertyPartsForMSBuildTask(
                ImmutableDictionary<string, string> properties,
                ImmutableDictionary<string, string> additionalProperties,
                ImmutableList<string> undefineProperties)
            {
                Properties = properties;
                AdditionalProperties = additionalProperties;
                UndefineProperties = undefineProperties;
            }
 
            public bool AllEmpty()
            {
                return Properties.Count == 0 && AdditionalProperties.Count == 0 && UndefineProperties.Count == 0;
            }
 
            public GlobalPropertyPartsForMSBuildTask AddPropertyToUndefine(string propertyToUndefine)
            {
                return new GlobalPropertyPartsForMSBuildTask(Properties, AdditionalProperties, UndefineProperties.Add(propertyToUndefine));
            }
        }
 
        private delegate GlobalPropertyPartsForMSBuildTask GlobalPropertiesModifier(GlobalPropertyPartsForMSBuildTask defaultParts, ProjectItemInstance projectReference);
 
        /// <summary>
        ///     Gets the effective global properties for an item that will get passed to <see cref="MSBuild.Projects"/>.
        /// </summary>
        /// <remarks>
        ///     The behavior of this method matches the hardcoded behaviour of the msbuild task
        ///     and the <paramref name="globalPropertyModifiers"/> parameter can contain other mutations done at build time in targets / tasks
        /// </remarks>
        private static PropertyDictionary<ProjectPropertyInstance> GetGlobalPropertiesForItem(
            ProjectItemInstance projectReference,
            PropertyDictionary<ProjectPropertyInstance> requesterGlobalProperties,
            bool allowCollectionReuse,
            IEnumerable<GlobalPropertiesModifier> globalPropertyModifiers)
        {
            ErrorUtilities.VerifyThrowInternalNull(projectReference, nameof(projectReference));
            ErrorUtilities.VerifyThrowArgumentNull(requesterGlobalProperties, nameof(requesterGlobalProperties));
 
            var properties = SplitPropertyNameValuePairs(ItemMetadataNames.PropertiesMetadataName, projectReference.GetMetadataValue(ItemMetadataNames.PropertiesMetadataName));
            var additionalProperties = SplitPropertyNameValuePairs(ItemMetadataNames.AdditionalPropertiesMetadataName, projectReference.GetMetadataValue(ItemMetadataNames.AdditionalPropertiesMetadataName));
            var undefineProperties = SplitPropertyNames(projectReference.GetMetadataValue(ItemMetadataNames.UndefinePropertiesMetadataName));
 
            var defaultParts = new GlobalPropertyPartsForMSBuildTask(properties.ToImmutableDictionary(), additionalProperties.ToImmutableDictionary(), undefineProperties.ToImmutableList());
 
            var globalPropertyParts = globalPropertyModifiers?.Aggregate(defaultParts, (currentProperties, modifier) => modifier(currentProperties, projectReference)) ?? defaultParts;
 
            if (globalPropertyParts.AllEmpty() && allowCollectionReuse)
            {
                return requesterGlobalProperties;
            }
 
            // Make a copy to avoid mutating the requester
            var globalProperties = new PropertyDictionary<ProjectPropertyInstance>(requesterGlobalProperties);
 
            // Append and remove properties as specified by the various metadata
            MergeIntoPropertyDictionary(globalProperties, globalPropertyParts.Properties);
            MergeIntoPropertyDictionary(globalProperties, globalPropertyParts.AdditionalProperties);
            RemoveFromPropertyDictionary(globalProperties, globalPropertyParts.UndefineProperties);
 
            return globalProperties;
        }
 
        private static void MergeIntoPropertyDictionary(
            PropertyDictionary<ProjectPropertyInstance> destination,
            IReadOnlyDictionary<string, string> source)
        {
            foreach (var pair in source)
            {
                destination[pair.Key] = ProjectPropertyInstance.Create(pair.Key, pair.Value);
            }
        }
 
        private static IReadOnlyDictionary<string, string> SplitPropertyNameValuePairs(string syntaxName, string propertyNameAndValuesString)
        {
            if (String.IsNullOrEmpty(propertyNameAndValuesString))
            {
                return ImmutableDictionary<string, string>.Empty;
            }
 
            if (PropertyParser.GetTableWithEscaping(
                null,
                null,
                null,
                propertyNameAndValuesString.Split(PropertySeparator, StringSplitOptions.RemoveEmptyEntries),
                out var propertiesTable))
            {
                return propertiesTable;
            }
 
            throw new InvalidProjectFileException(
                String.Format(
                    CultureInfo.InvariantCulture,
                    ResourceUtilities.GetResourceString("General.InvalidPropertyError"),
                    syntaxName,
                    propertyNameAndValuesString));
        }
 
        private static IReadOnlyCollection<string> SplitPropertyNames(string propertyNamesString)
        {
            if (String.IsNullOrEmpty(propertyNamesString))
            {
                return ImmutableArray<string>.Empty;
            }
 
            return propertyNamesString.Split(PropertySeparator, StringSplitOptions.RemoveEmptyEntries);
        }
 
        private static void RemoveFromPropertyDictionary(
            PropertyDictionary<ProjectPropertyInstance> properties,
            IReadOnlyCollection<string> propertyNamesToRemove)
        {
            foreach (var propertyName in propertyNamesToRemove)
            {
                properties.Remove(propertyName);
            }
        }
 
        public readonly struct TargetsToPropagate
        {
            private readonly ImmutableList<TargetSpecification> _outerBuildTargets;
            private readonly ImmutableList<TargetSpecification> _allTargets;
 
            private TargetsToPropagate(ImmutableList<TargetSpecification> outerBuildTargets, ImmutableList<TargetSpecification> nonOuterBuildTargets)
            {
                _outerBuildTargets = outerBuildTargets;
 
                // This is used as the list of entry targets for both inner builds and non-multitargeting projects.
                // It represents the concatenation of outer build targets and non outer build targets, in this order.
                // Non-multitargeting projects use these targets because they act as both outer and inner builds.
                _allTargets = outerBuildTargets.AddRange(nonOuterBuildTargets);
            }
 
            /// <summary>
            /// Given a project and a set of entry targets the project would get called with,
            /// parse the project's project reference target specification and compute how the target would call its references.
            ///
            /// The calling code should then call <see cref="GetApplicableTargetsForReference"/> for each of the project's references
            /// to get the concrete targets for each reference.
            /// </summary>
            /// <param name="project">Project containing the PRT protocol</param>
            /// <param name="entryTargets">Targets with which <paramref name="project"/> will get called</param>
            /// <returns></returns>
            public static TargetsToPropagate FromProjectAndEntryTargets(ProjectInstance project, ImmutableList<string> entryTargets)
            {
                ImmutableList<TargetSpecification>.Builder targetsForOuterBuild = ImmutableList.CreateBuilder<TargetSpecification>();
                ImmutableList<TargetSpecification>.Builder targetsForInnerBuild = ImmutableList.CreateBuilder<TargetSpecification>();
 
                ICollection<ProjectItemInstance> projectReferenceTargets = project.GetItems(ItemTypeNames.ProjectReferenceTargets);
 
                foreach (string entryTarget in entryTargets)
                {
                    foreach (ProjectItemInstance projectReferenceTarget in projectReferenceTargets)
                    {
                        if (projectReferenceTarget.EvaluatedInclude.Equals(entryTarget, StringComparison.OrdinalIgnoreCase))
                        {
                            string targetsMetadataValue = projectReferenceTarget.GetMetadataValue(ItemMetadataNames.ProjectReferenceTargetsMetadataName);
                            bool skipNonexistentTargets = MSBuildStringIsTrue(projectReferenceTarget.GetMetadataValue("SkipNonexistentTargets"));
                            bool targetsAreForOuterBuild = MSBuildStringIsTrue(projectReferenceTarget.GetMetadataValue(ProjectReferenceTargetIsOuterBuildMetadataName));
                            TargetSpecification[] targets = ExpressionShredder.SplitSemiColonSeparatedList(targetsMetadataValue)
                                .Select(t => new TargetSpecification(t, skipNonexistentTargets)).ToArray();
                            if (targetsAreForOuterBuild)
                            {
                                targetsForOuterBuild.AddRange(targets);
                            }
                            else
                            {
                                targetsForInnerBuild.AddRange(targets);
                            }
                        }
                    }
                }
 
                return new TargetsToPropagate(targetsForOuterBuild.ToImmutable(), targetsForInnerBuild.ToImmutable());
            }
 
            public ImmutableList<string> GetApplicableTargetsForReference(ProjectGraphNode projectGraphNode)
            {
                ImmutableList<string> RemoveNonexistentTargetsIfSkippable(ImmutableList<TargetSpecification> targets)
                {
                    // Keep targets that are non-skippable or that exist but are skippable.
                    return targets
                        .Where(t => !t.SkipIfNonexistent || projectGraphNode.ProjectInstance.Targets.ContainsKey(t.Target))
                        .Select(t => t.Target)
                        .ToImmutableList();
                }
 
                return projectGraphNode.ProjectType switch
                {
                    ProjectType.InnerBuild => RemoveNonexistentTargetsIfSkippable(_allTargets),
                    ProjectType.OuterBuild => RemoveNonexistentTargetsIfSkippable(_outerBuildTargets),
                    ProjectType.NonMultitargeting => RemoveNonexistentTargetsIfSkippable(_allTargets),
                    _ => throw new ArgumentOutOfRangeException(),
                };
            }
        }
 
        public bool RequiresTransitiveProjectReferences(ProjectGraphNode projectGraphNode)
        {
            // Outer builds do not get edges based on ProjectReference or their transitive closure, only inner builds do.
            if (projectGraphNode.ProjectType == ProjectType.OuterBuild)
            {
                return false;
            }
 
            ProjectInstance projectInstance = projectGraphNode.ProjectInstance;
 
            // special case for Quickbuild which updates msbuild binaries independent of props/targets. Remove this when all QB repos will have
            // migrated to new enough Visual Studio versions whose Microsoft.Managed.After.Targets enable transitive references.
            if (string.IsNullOrWhiteSpace(projectInstance.GetEngineRequiredPropertyValue(AddTransitiveProjectReferencesInStaticGraphPropertyName)) &&
                MSBuildStringIsTrue(projectInstance.GetEngineRequiredPropertyValue("UsingMicrosoftNETSdk")) &&
                MSBuildStringIsFalse(projectInstance.GetEngineRequiredPropertyValue("DisableTransitiveProjectReferences")))
            {
                return true;
            }
 
            return MSBuildStringIsTrue(
                projectInstance.GetEngineRequiredPropertyValue(AddTransitiveProjectReferencesInStaticGraphPropertyName));
        }
 
        private static bool MSBuildStringIsTrue(string msbuildString) =>
            ConversionUtilities.ConvertStringToBool(msbuildString, nullOrWhitespaceIsFalse: true);
 
        private static bool MSBuildStringIsFalse(string msbuildString) => !MSBuildStringIsTrue(msbuildString);
    }
}