File: ValidateProjectDataPackagesTask.cs
Web Access
Project: Microsoft.NET.ProjectData.Tasks.csproj (Microsoft.NET.ProjectData.Tasks)
// 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.Text.Json;
using Microsoft.Build.Framework;
using NuGet.Versioning;

namespace Microsoft.NET.ProjectData.Tasks;

/// <summary>
/// Validates that evaluated package references agree with the resolved restore graph and that
/// resolved package folders still exist.
/// </summary>
public sealed class ValidateProjectDataPackagesTask : Microsoft.Build.Utilities.Task
{
	[Required]
	public string ProjectFilePath { get; set; } = string.Empty;

	public string AssetsFile { get; set; } = string.Empty;

	public string TargetFramework { get; set; } = string.Empty;

	public string TargetFrameworkMoniker { get; set; } = string.Empty;

	public bool ManagePackageVersionsCentrally { get; set; }

	public bool CentralPackageTransitivePinningEnabled { get; set; }

	public ITaskItem[] PackageReferences { get; set; } = [];

	public ITaskItem[] PackageVersions { get; set; } = [];

	public ITaskItem[] ResolvedPackages { get; set; } = [];

	public override bool Execute()
	{
		Dictionary<string, ITaskItem> resolvedPackagesById = this.GetResolvedPackagesById();
		Dictionary<string, string> centralVersionsById = this.GetCentralVersionsById();
		RestoreGraphRequests? restoredRequests = this.GetRestoreGraphRequests();
		Dictionary<string, string>? restoredRequestedVersionsById = restoredRequests?.DirectRequests;
		var currentPackageIds = new HashSet<string>(
			this.PackageReferences.Select(static packageReference => packageReference.ItemSpec),
			StringComparer.OrdinalIgnoreCase);
		List<string> missingPackages = [];
		List<string> missingRequestedVersions = [];
		List<string> incompatiblePackages = [];
		List<string> staleRequestedVersions = [];
		List<string> incompatibleCentralTransitiveVersions = [];
		List<string> staleCentralTransitiveVersions = [];
		bool centralTransitivePinningModeChanged = restoredRequests?.CentralPackageTransitivePinningEnabled is bool restoredPinningEnabled &&
			restoredPinningEnabled != this.CentralPackageTransitivePinningEnabled;

		foreach (ITaskItem packageReference in this.PackageReferences)
		{
			if (string.Equals(packageReference.GetMetadata("IsImplicitlyDefined"), "true", StringComparison.OrdinalIgnoreCase))
			{
				continue;
			}

			string packageId = packageReference.ItemSpec;
			if (!resolvedPackagesById.TryGetValue(packageId, out ITaskItem? resolvedPackage))
			{
				missingPackages.Add(packageId);
				continue;
			}

			string requestedVersion = this.GetRequestedVersion(packageReference, centralVersionsById);
			if (string.IsNullOrWhiteSpace(requestedVersion))
			{
				missingRequestedVersions.Add(packageId);
				continue;
			}

			string resolvedVersion = GetResolvedVersion(resolvedPackage);
			if (!VersionRange.TryParse(requestedVersion, out VersionRange? requestedRange))
			{
				incompatiblePackages.Add($"{packageId} (invalid requested version '{requestedVersion}')");
				continue;
			}

			if (restoredRequestedVersionsById is not null)
			{
				if (!restoredRequestedVersionsById.TryGetValue(packageId, out string? restoredRequestedVersion))
				{
					staleRequestedVersions.Add($"{packageId} (current request '{requestedVersion}', missing from restored requests)");
				}
				else if (!VersionRange.TryParse(restoredRequestedVersion, out VersionRange? restoredRequestedRange) ||
					!requestedRange.Equals(restoredRequestedRange))
				{
					staleRequestedVersions.Add($"{packageId} (current request '{requestedVersion}', restored request '{restoredRequestedVersion}')");
				}
				else if (restoredRequests!.DirectAssetSelections.TryGetValue(packageId, out PackageAssetSelection? restoredAssetSelection))
				{
					PackageAssetSelection currentAssetSelection = GetCurrentAssetSelection(packageReference);
					if (!currentAssetSelection.Equals(restoredAssetSelection))
					{
						staleRequestedVersions.Add(
							$"{packageId} (current assets '{currentAssetSelection}', restored assets '{restoredAssetSelection}')");
					}
				}
			}

			if (!NuGetVersion.TryParse(resolvedVersion, out NuGetVersion? resolvedNuGetVersion))
			{
				incompatiblePackages.Add($"{packageId} (requested '{requestedVersion}', invalid resolved version '{resolvedVersion}')");
			}
			else if (!requestedRange.Satisfies(resolvedNuGetVersion))
			{
				incompatiblePackages.Add($"{packageId} (requested '{requestedVersion}', resolved '{resolvedVersion}')");
			}
			else if (requestedRange.Float is not null && !requestedRange.Float.Satisfies(resolvedNuGetVersion))
			{
				incompatiblePackages.Add($"{packageId} (requested '{requestedVersion}', resolved '{resolvedVersion}')");
			}
		}

		if (this.CentralPackageTransitivePinningEnabled && restoredRequests is not null)
		{
			var activeCentralTransitiveVersionsById = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			foreach (KeyValuePair<string, string> centralVersion in centralVersionsById)
			{
				if (!currentPackageIds.Contains(centralVersion.Key) &&
					restoredRequests.ResolvedVersions.TryGetValue(centralVersion.Key, out string? centralResolvedVersion) &&
					(restoredRequests.CentralTransitiveRequests.ContainsKey(centralVersion.Key) ||
						!restoredRequests.CentralVersions.TryGetValue(centralVersion.Key, out string? restoredCentralVersion) ||
						!AreEquivalentVersionRanges(centralVersion.Value, restoredCentralVersion)))
				{
					activeCentralTransitiveVersionsById[centralVersion.Key] = centralVersion.Value;
					this.ValidateCentralTransitiveVersion(
						centralVersion.Key,
						centralVersion.Value,
						centralResolvedVersion,
						incompatibleCentralTransitiveVersions);
				}
			}

			foreach (KeyValuePair<string, string> currentRequest in activeCentralTransitiveVersionsById)
			{
				if (!restoredRequests.CentralTransitiveRequests.TryGetValue(currentRequest.Key, out string? restoredRequest))
				{
					staleCentralTransitiveVersions.Add($"{currentRequest.Key} (current request '{currentRequest.Value}', missing from restored central transitive requests)");
				}
				else if (!AreEquivalentVersionRanges(currentRequest.Value, restoredRequest))
				{
					staleCentralTransitiveVersions.Add($"{currentRequest.Key} (current request '{currentRequest.Value}', restored request '{restoredRequest}')");
				}
			}

			foreach (KeyValuePair<string, string> restoredRequest in restoredRequests.CentralTransitiveRequests)
			{
				if (!activeCentralTransitiveVersionsById.ContainsKey(restoredRequest.Key))
				{
					staleCentralTransitiveVersions.Add($"{restoredRequest.Key} (restored request '{restoredRequest.Value}', no current active central transitive pin)");
				}
			}
		}

		if (restoredRequestedVersionsById is not null)
		{
			foreach (KeyValuePair<string, string> restoredRequest in restoredRequestedVersionsById)
			{
				if (!currentPackageIds.Contains(restoredRequest.Key) &&
					!restoredRequests!.AutoReferencedRequestIds.Contains(restoredRequest.Key))
				{
					staleRequestedVersions.Add($"{restoredRequest.Key} (restored request '{restoredRequest.Value}', no current PackageReference)");
				}
			}
		}

		if (missingPackages.Count > 0)
		{
			this.Log.LogError(
				"ProjectData: cannot write project data for '{0}' because the restore graph does not contain declared PackageReference items: {1}. Run restore successfully before ProjectDataBuild.",
				this.ProjectFilePath,
				string.Join("; ", missingPackages.OrderBy(static packageId => packageId, StringComparer.OrdinalIgnoreCase)));
		}

		if (missingRequestedVersions.Count > 0)
		{
			this.Log.LogError(
				"ProjectData: cannot write project data for '{0}' because declared PackageReference items have no evaluated version request: {1}. Restore may be stale or central package version metadata may be missing. Run restore successfully before ProjectDataBuild.",
				this.ProjectFilePath,
				string.Join("; ", missingRequestedVersions.OrderBy(static packageId => packageId, StringComparer.OrdinalIgnoreCase)));
		}

		if (incompatiblePackages.Count > 0)
		{
			this.Log.LogError(
				"ProjectData: cannot write project data for '{0}' because declared PackageReference versions are not satisfied by the restore graph: {1}. Run restore successfully before ProjectDataBuild.",
				this.ProjectFilePath,
				string.Join("; ", incompatiblePackages.OrderBy(static package => package, StringComparer.OrdinalIgnoreCase)));
		}

		if (staleRequestedVersions.Count > 0)
		{
			this.Log.LogError(
				"ProjectData: cannot write project data for '{0}' because declared PackageReference requests differ from the restore graph: {1}. Run restore successfully before ProjectDataBuild.",
				this.ProjectFilePath,
				string.Join("; ", staleRequestedVersions.OrderBy(static package => package, StringComparer.OrdinalIgnoreCase)));
		}

		if (incompatibleCentralTransitiveVersions.Count > 0)
		{
			this.Log.LogError(
				"ProjectData: cannot write project data for '{0}' because central transitive package version requests are not satisfied by the restore graph: {1}. Run restore successfully before ProjectDataBuild.",
				this.ProjectFilePath,
				string.Join("; ", incompatibleCentralTransitiveVersions.OrderBy(static package => package, StringComparer.OrdinalIgnoreCase)));
		}

		if (staleCentralTransitiveVersions.Count > 0)
		{
			this.Log.LogError(
				"ProjectData: cannot write project data for '{0}' because central transitive package version requests differ from the restore graph: {1}. Run restore successfully before ProjectDataBuild.",
				this.ProjectFilePath,
				string.Join("; ", staleCentralTransitiveVersions.OrderBy(static package => package, StringComparer.OrdinalIgnoreCase)));
		}

		if (centralTransitivePinningModeChanged)
		{
			this.Log.LogError(
				"ProjectData: cannot write project data for '{0}' because central transitive package pinning mode differs from the restore graph: current '{1}', restored '{2}'. Run restore successfully before ProjectDataBuild.",
				this.ProjectFilePath,
				this.CentralPackageTransitivePinningEnabled,
				restoredRequests!.CentralPackageTransitivePinningEnabled);
		}

		return !this.Log.HasLoggedErrors;
	}

	private void ValidateCentralTransitiveVersion(
		string packageId,
		string requestedVersion,
		string resolvedVersion,
		List<string> incompatiblePackages)
	{
		if (!VersionRange.TryParse(requestedVersion, out VersionRange? requestedRange))
		{
			incompatiblePackages.Add($"{packageId} (invalid requested version '{requestedVersion}')");
			return;
		}

		if (!NuGetVersion.TryParse(resolvedVersion, out NuGetVersion? resolvedNuGetVersion))
		{
			incompatiblePackages.Add($"{packageId} (requested '{requestedVersion}', invalid resolved version '{resolvedVersion}')");
		}
		else if (!requestedRange.Satisfies(resolvedNuGetVersion) ||
			(requestedRange.Float is not null && !requestedRange.Float.Satisfies(resolvedNuGetVersion)))
		{
			incompatiblePackages.Add($"{packageId} (requested '{requestedVersion}', resolved '{resolvedVersion}')");
		}
	}

	private Dictionary<string, ITaskItem> GetResolvedPackagesById()
	{
		var result = new Dictionary<string, ITaskItem>(StringComparer.OrdinalIgnoreCase);
		foreach (ITaskItem resolvedPackage in this.ResolvedPackages)
		{
			string packageId = resolvedPackage.GetMetadata("Name");
			if (string.IsNullOrWhiteSpace(packageId))
			{
				packageId = GetIdentityPart(resolvedPackage.ItemSpec);
			}

			if (!string.IsNullOrWhiteSpace(packageId))
			{
				result[packageId] = resolvedPackage;
			}

			string packagePath = resolvedPackage.GetMetadata("Path");
			if (string.IsNullOrWhiteSpace(packagePath))
			{
				this.Log.LogError(
					"ProjectData: cannot write project data for '{0}' because resolved package '{1}' has no package path. Run restore successfully before ProjectDataBuild.",
					this.ProjectFilePath,
					resolvedPackage.ItemSpec);
			}
			else if (!Directory.Exists(packagePath))
			{
				this.Log.LogError(
					"ProjectData: cannot write project data for '{0}' because package files are missing: {1} at {2}. Run restore successfully before ProjectDataBuild.",
					this.ProjectFilePath,
					resolvedPackage.ItemSpec,
					packagePath);
			}
		}

		return result;
	}

	private Dictionary<string, string> GetCentralVersionsById()
	{
		var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
		foreach (ITaskItem packageVersion in this.PackageVersions)
		{
			string version = packageVersion.GetMetadata("Version");
			if (!string.IsNullOrWhiteSpace(packageVersion.ItemSpec) && !string.IsNullOrWhiteSpace(version))
			{
				result[packageVersion.ItemSpec] = version;
			}
		}

		return result;
	}

	private RestoreGraphRequests? GetRestoreGraphRequests()
	{
		if (string.IsNullOrWhiteSpace(this.AssetsFile) || string.IsNullOrWhiteSpace(this.TargetFramework))
		{
			return null;
		}

		try
		{
			using FileStream stream = File.OpenRead(this.AssetsFile);
			using JsonDocument assetsFile = JsonDocument.Parse(stream);
			if (!TryGetProperty(assetsFile.RootElement, "project", out JsonElement project) ||
				!TryGetProperty(project, "frameworks", out JsonElement frameworks) ||
				!TryGetProperty(frameworks, this.TargetFramework, out JsonElement framework))
			{
				this.Log.LogError(
					"ProjectData: cannot write project data for '{0}' because restore graph '{1}' does not contain dependency requests for target framework '{2}'. Run restore successfully before ProjectDataBuild.",
					this.ProjectFilePath,
					this.AssetsFile,
					this.TargetFramework);
				return null;
			}

			var directRequests = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			var directAssetSelections = new Dictionary<string, PackageAssetSelection>(StringComparer.OrdinalIgnoreCase);
			var autoReferencedRequestIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			if (framework.ValueKind != JsonValueKind.Object)
			{
				this.LogAssetsFileError($"target framework '{this.TargetFramework}' must be a JSON object");
				return null;
			}

			bool hasCentralPackageEvidence = this.ManagePackageVersionsCentrally ||
				this.CentralPackageTransitivePinningEnabled ||
				this.PackageVersions.Length > 0 ||
				TryGetProperty(framework, "centralPackageVersions", out _) ||
				TryGetProperty(assetsFile.RootElement, "centralTransitiveDependencyGroups", out _);
			bool? restoredPinningEnabled = this.GetRestoredCentralPackageTransitivePinningMode(project, hasCentralPackageEvidence);
			if (hasCentralPackageEvidence && restoredPinningEnabled is null)
			{
				return null;
			}

			if (!TryGetProperty(framework, "dependencies", out JsonElement dependencies))
			{
				dependencies = default;
			}
			else if (dependencies.ValueKind != JsonValueKind.Object)
			{
				this.LogAssetsFileError($"dependency requests for target framework '{this.TargetFramework}' must be a JSON object");
				return null;
			}

			if (dependencies.ValueKind == JsonValueKind.Object)
			{
				foreach (JsonProperty dependency in dependencies.EnumerateObject())
				{
					if (!TryGetStringProperty(dependency.Value, "target", out string target))
					{
						this.LogAssetsFileError($"dependency request '{dependency.Name}' for target framework '{this.TargetFramework}' has no string target");
						return null;
					}

					if (!string.Equals(target, "Package", StringComparison.OrdinalIgnoreCase))
					{
						continue;
					}

					if (!TryGetStringProperty(dependency.Value, "version", out string version) ||
						string.IsNullOrWhiteSpace(version))
					{
						this.LogAssetsFileError($"package dependency request '{dependency.Name}' for target framework '{this.TargetFramework}' has no string version");
						return null;
					}

					if (TryGetBooleanProperty(dependency.Value, "autoReferenced", out bool autoReferenced) &&
						autoReferenced)
					{
						autoReferencedRequestIds.Add(dependency.Name);
					}

					directRequests[dependency.Name] = version;
					if (!this.TryGetRestoredAssetSelection(dependency, this.TargetFramework, out PackageAssetSelection? assetSelection))
					{
						return null;
					}

					directAssetSelections[dependency.Name] = assetSelection;
				}
			}

			Dictionary<string, string>? restoredCentralVersions = this.GetRestoredCentralVersions(framework);
			Dictionary<string, string>? centralTransitiveRequests = this.GetRestoredCentralTransitiveRequests(
				assetsFile.RootElement,
				directRequests);
			Dictionary<string, string>? resolvedVersions = this.GetRestoredResolvedVersions(assetsFile.RootElement);
			return restoredCentralVersions is null || centralTransitiveRequests is null || resolvedVersions is null
				? null
				: new RestoreGraphRequests(
					directRequests,
					directAssetSelections,
					autoReferencedRequestIds,
					restoredCentralVersions,
					centralTransitiveRequests,
					resolvedVersions,
					restoredPinningEnabled);
		}
		catch (IOException ex)
		{
			this.LogAssetsFileError(ex.Message);
		}
		catch (UnauthorizedAccessException ex)
		{
			this.LogAssetsFileError(ex.Message);
		}
		catch (JsonException ex)
		{
			this.LogAssetsFileError(ex.Message);
		}

		return null;
	}

	private bool TryGetRestoredAssetSelection(
		JsonProperty dependency,
		string targetFramework,
		out PackageAssetSelection assetSelection)
	{
		PackageAssetFlags include = PackageAssetFlags.All;
		if (TryGetProperty(dependency.Value, "include", out JsonElement includeElement))
		{
			if (includeElement.ValueKind != JsonValueKind.String)
			{
				this.LogAssetsFileError($"package dependency request '{dependency.Name}' for target framework '{targetFramework}' has no string include assets");
				assetSelection = null!;
				return false;
			}

			include = ParseRestoredAssetFlags(includeElement.GetString(), PackageAssetFlags.All);
		}

		PackageAssetFlags suppressParent = PackageAssetSelection.DefaultSuppressParent;
		if (TryGetProperty(dependency.Value, "suppressParent", out JsonElement suppressParentElement))
		{
			if (suppressParentElement.ValueKind != JsonValueKind.String)
			{
				this.LogAssetsFileError($"package dependency request '{dependency.Name}' for target framework '{targetFramework}' has no string private assets");
				assetSelection = null!;
				return false;
			}

			suppressParent = ParseRestoredAssetFlags(suppressParentElement.GetString(), PackageAssetSelection.DefaultSuppressParent);
		}

		assetSelection = new PackageAssetSelection(include, suppressParent);
		return true;
	}

	private bool? GetRestoredCentralPackageTransitivePinningMode(JsonElement project, bool required)
	{
		if (!TryGetProperty(project, "restore", out JsonElement restore))
		{
			if (required)
			{
				this.LogAssetsFileError("restore settings do not contain central transitive package pinning mode");
			}

			return null;
		}

		if (restore.ValueKind != JsonValueKind.Object)
		{
			this.LogAssetsFileError("restore settings must be a JSON object");
			return null;
		}

		if (!TryGetProperty(restore, "CentralPackageTransitivePinningEnabled", out JsonElement pinningEnabled))
		{
			// NuGet 6.13 writes the property only when enabled; omission records false.
			return required ? false : null;
		}

		if (pinningEnabled.ValueKind != JsonValueKind.True &&
			pinningEnabled.ValueKind != JsonValueKind.False)
		{
			this.LogAssetsFileError("central transitive package pinning mode in restore settings must be a JSON boolean");
			return null;
		}

		return pinningEnabled.GetBoolean();
	}

	private Dictionary<string, string>? GetRestoredCentralVersions(JsonElement framework)
	{
		var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
		if (!this.CentralPackageTransitivePinningEnabled ||
			!TryGetProperty(framework, "centralPackageVersions", out JsonElement centralVersions))
		{
			return result;
		}

		if (centralVersions.ValueKind != JsonValueKind.Object)
		{
			this.LogAssetsFileError($"central package versions for target framework '{this.TargetFramework}' must be a JSON object");
			return null;
		}

		foreach (JsonProperty centralVersion in centralVersions.EnumerateObject())
		{
			if (centralVersion.Value.ValueKind != JsonValueKind.String ||
				string.IsNullOrWhiteSpace(centralVersion.Value.GetString()))
			{
				this.LogAssetsFileError($"central package version '{centralVersion.Name}' for target framework '{this.TargetFramework}' has no string version");
				return null;
			}

			result[centralVersion.Name] = centralVersion.Value.GetString()!;
		}

		return result;
	}

	private Dictionary<string, string>? GetRestoredCentralTransitiveRequests(
		JsonElement assetsFile,
		IReadOnlyDictionary<string, string> directRequests)
	{
		var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
		if (!this.CentralPackageTransitivePinningEnabled ||
			!TryGetProperty(assetsFile, "centralTransitiveDependencyGroups", out JsonElement groups))
		{
			return result;
		}

		if (groups.ValueKind != JsonValueKind.Object)
		{
			this.LogAssetsFileError("central transitive dependency groups must be a JSON object");
			return null;
		}

		if (!this.TryGetTargetFrameworkGroup(groups, out JsonElement group))
		{
			return result;
		}

		if (group.ValueKind != JsonValueKind.Object)
		{
			this.LogAssetsFileError($"central transitive dependency group for target framework '{this.TargetFramework}' must be a JSON object");
			return null;
		}

		foreach (JsonProperty dependency in group.EnumerateObject())
		{
			if (directRequests.ContainsKey(dependency.Name))
			{
				continue;
			}

			if (!TryGetStringProperty(dependency.Value, "version", out string version) ||
				string.IsNullOrWhiteSpace(version))
			{
				this.LogAssetsFileError($"central transitive dependency request '{dependency.Name}' for target framework '{this.TargetFramework}' has no string version");
				return null;
			}

			result[dependency.Name] = version;
		}

		return result;
	}

	private Dictionary<string, string>? GetRestoredResolvedVersions(JsonElement assetsFile)
	{
		var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
		if (!this.CentralPackageTransitivePinningEnabled)
		{
			return result;
		}

		if (!TryGetProperty(assetsFile, "targets", out JsonElement targets) ||
			targets.ValueKind != JsonValueKind.Object)
		{
			this.LogAssetsFileError("resolved target graphs must be a JSON object");
			return null;
		}

		if (!this.TryGetTargetFrameworkGroup(targets, out JsonElement target))
		{
			this.LogAssetsFileError($"resolved target graphs do not contain target framework '{this.TargetFramework}'");
			return null;
		}

		if (target.ValueKind != JsonValueKind.Object)
		{
			this.LogAssetsFileError($"resolved target graph for target framework '{this.TargetFramework}' must be a JSON object");
			return null;
		}

		foreach (JsonProperty dependency in target.EnumerateObject())
		{
			if (!TryGetStringProperty(dependency.Value, "type", out string dependencyType))
			{
				this.LogAssetsFileError($"resolved dependency '{dependency.Name}' for target framework '{this.TargetFramework}' has no string type");
				return null;
			}

			if (!string.Equals(dependencyType, "package", StringComparison.OrdinalIgnoreCase))
			{
				continue;
			}

			int separatorIndex = dependency.Name.LastIndexOf('/');
			if (separatorIndex <= 0 || separatorIndex == dependency.Name.Length - 1)
			{
				this.LogAssetsFileError($"resolved package identity '{dependency.Name}' for target framework '{this.TargetFramework}' has no concrete version");
				return null;
			}

			result[dependency.Name.Substring(0, separatorIndex)] = dependency.Name.Substring(separatorIndex + 1);
		}

		return result;
	}

	private bool TryGetTargetFrameworkGroup(JsonElement groups, out JsonElement group)
	{
		if (!string.IsNullOrWhiteSpace(this.TargetFrameworkMoniker) &&
			TryGetProperty(groups, this.TargetFrameworkMoniker, out group))
		{
			return true;
		}

		return TryGetProperty(groups, this.TargetFramework, out group);
	}

	private void LogAssetsFileError(string message)
	{
		this.Log.LogError(
			"ProjectData: cannot write project data for '{0}' because restore graph '{1}' could not be read: {2}. Run restore successfully before ProjectDataBuild.",
			this.ProjectFilePath,
			this.AssetsFile,
			message);
	}

	private string GetRequestedVersion(ITaskItem packageReference, IReadOnlyDictionary<string, string> centralVersionsById)
	{
		string versionOverride = packageReference.GetMetadata("VersionOverride");
		if (this.ManagePackageVersionsCentrally && !string.IsNullOrWhiteSpace(versionOverride))
		{
			return versionOverride;
		}

		string version = packageReference.GetMetadata("Version");
		if (!string.IsNullOrWhiteSpace(version))
		{
			return version;
		}

		return this.ManagePackageVersionsCentrally &&
			centralVersionsById.TryGetValue(packageReference.ItemSpec, out string? centralVersion)
				? centralVersion
				: string.Empty;
	}

	private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement value)
	{
		if (element.ValueKind != JsonValueKind.Object)
		{
			value = default;
			return false;
		}

		foreach (JsonProperty property in element.EnumerateObject())
		{
			if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
			{
				value = property.Value;
				return true;
			}
		}

		value = default;
		return false;
	}

	private static bool TryGetBooleanProperty(JsonElement element, string propertyName, out bool value)
	{
		if (element.ValueKind == JsonValueKind.Object &&
			element.TryGetProperty(propertyName, out JsonElement property) &&
			(property.ValueKind == JsonValueKind.True || property.ValueKind == JsonValueKind.False))
		{
			value = property.GetBoolean();
			return true;
		}

		value = false;
		return false;
	}

	private static bool TryGetStringProperty(JsonElement element, string propertyName, out string value)
	{
		if (element.ValueKind == JsonValueKind.Object &&
			element.TryGetProperty(propertyName, out JsonElement property) &&
			property.ValueKind == JsonValueKind.String)
		{
			value = property.GetString() ?? string.Empty;
			return true;
		}

		value = string.Empty;
		return false;
	}

	private static bool AreEquivalentVersionRanges(string left, string right)
		=> VersionRange.TryParse(left, out VersionRange? leftRange) &&
			VersionRange.TryParse(right, out VersionRange? rightRange) &&
			leftRange.Equals(rightRange);

	private static PackageAssetSelection GetCurrentAssetSelection(ITaskItem packageReference)
	{
		PackageAssetFlags include = ParseEvaluatedAssetFlags(
			packageReference.GetMetadata("IncludeAssets"),
			PackageAssetFlags.All);
		PackageAssetFlags exclude = ParseEvaluatedAssetFlags(
			packageReference.GetMetadata("ExcludeAssets"),
			PackageAssetFlags.None);
		PackageAssetFlags suppressParent = ParseEvaluatedAssetFlags(
			packageReference.GetMetadata("PrivateAssets"),
			PackageAssetSelection.DefaultSuppressParent);
		return new PackageAssetSelection(include & ~exclude, suppressParent);
	}

	private static PackageAssetFlags ParseEvaluatedAssetFlags(string? value, PackageAssetFlags defaultValue)
		=> ParseAssetFlags(value, defaultValue, ';', expandBuildTransitive: true);

	private static PackageAssetFlags ParseRestoredAssetFlags(string? value, PackageAssetFlags defaultValue)
		=> ParseAssetFlags(value, defaultValue, ',', expandBuildTransitive: false);

	private static PackageAssetFlags ParseAssetFlags(
		string? value,
		PackageAssetFlags defaultValue,
		char separator,
		bool expandBuildTransitive)
	{
		if (string.IsNullOrWhiteSpace(value))
		{
			return defaultValue;
		}

		PackageAssetFlags result = PackageAssetFlags.None;
		bool hasToken = false;
		foreach (string part in value!.Split([separator], StringSplitOptions.RemoveEmptyEntries))
		{
			string token = part.Trim();
			if (token.Length == 0)
			{
				continue;
			}

			hasToken = true;
			switch (token.ToLowerInvariant())
			{
				case "all":
					result |= PackageAssetFlags.All;
					break;
				case "runtime":
					result |= PackageAssetFlags.Runtime;
					break;
				case "compile":
					result |= PackageAssetFlags.Compile;
					break;
				case "build":
					result |= PackageAssetFlags.Build;
					break;
				case "contentfiles":
					result |= PackageAssetFlags.ContentFiles;
					break;
				case "native":
					result |= PackageAssetFlags.Native;
					break;
				case "analyzers":
					result |= PackageAssetFlags.Analyzers;
					break;
				case "buildtransitive":
					result |= PackageAssetFlags.BuildTransitive;
					if (expandBuildTransitive)
					{
						result |= PackageAssetFlags.Build;
					}
					break;
			}
		}

		return hasToken ? result : defaultValue;
	}

	private static string GetResolvedVersion(ITaskItem resolvedPackage)
	{
		string version = resolvedPackage.GetMetadata("Version");
		if (!string.IsNullOrWhiteSpace(version))
		{
			return version;
		}

		int separatorIndex = resolvedPackage.ItemSpec.LastIndexOf('/');
		return separatorIndex >= 0 ? resolvedPackage.ItemSpec.Substring(separatorIndex + 1) : string.Empty;
	}

	private static string GetIdentityPart(string resolvedPackageIdentity)
	{
		int separatorIndex = resolvedPackageIdentity.LastIndexOf('/');
		return separatorIndex >= 0 ? resolvedPackageIdentity.Substring(0, separatorIndex) : resolvedPackageIdentity;
	}

	private sealed class RestoreGraphRequests
	{
		public RestoreGraphRequests(
			Dictionary<string, string> directRequests,
			Dictionary<string, PackageAssetSelection> directAssetSelections,
			HashSet<string> autoReferencedRequestIds,
			Dictionary<string, string> centralVersions,
			Dictionary<string, string> centralTransitiveRequests,
			Dictionary<string, string> resolvedVersions,
			bool? centralPackageTransitivePinningEnabled)
		{
			this.DirectRequests = directRequests;
			this.DirectAssetSelections = directAssetSelections;
			this.AutoReferencedRequestIds = autoReferencedRequestIds;
			this.CentralVersions = centralVersions;
			this.CentralTransitiveRequests = centralTransitiveRequests;
			this.ResolvedVersions = resolvedVersions;
			this.CentralPackageTransitivePinningEnabled = centralPackageTransitivePinningEnabled;
		}

		public Dictionary<string, string> DirectRequests { get; }

		public Dictionary<string, PackageAssetSelection> DirectAssetSelections { get; }

		public HashSet<string> AutoReferencedRequestIds { get; }

		public Dictionary<string, string> CentralVersions { get; }

		public Dictionary<string, string> CentralTransitiveRequests { get; }

		public Dictionary<string, string> ResolvedVersions { get; }

		public bool? CentralPackageTransitivePinningEnabled { get; }
	}

	[Flags]
	private enum PackageAssetFlags
	{
		None = 0,
		Runtime = 1 << 0,
		Compile = 1 << 1,
		Build = 1 << 2,
		ContentFiles = 1 << 3,
		Native = 1 << 4,
		Analyzers = 1 << 5,
		BuildTransitive = 1 << 6,
		All = Runtime | Compile | Build | ContentFiles | Native | Analyzers | BuildTransitive,
	}

	private sealed class PackageAssetSelection : IEquatable<PackageAssetSelection>
	{
		public const PackageAssetFlags DefaultSuppressParent =
			PackageAssetFlags.Build | PackageAssetFlags.ContentFiles | PackageAssetFlags.Analyzers;

		public PackageAssetSelection(PackageAssetFlags include, PackageAssetFlags suppressParent)
		{
			this.Include = include;
			this.SuppressParent = suppressParent;
		}

		public PackageAssetFlags Include { get; }

		public PackageAssetFlags SuppressParent { get; }

		public bool Equals(PackageAssetSelection? other)
			=> other is not null &&
				this.Include == other.Include &&
				this.SuppressParent == other.SuppressParent;

		public override bool Equals(object? obj) => this.Equals(obj as PackageAssetSelection);

		public override int GetHashCode() => ((int)this.Include * 397) ^ (int)this.SuppressParent;

		public override string ToString() => $"include={this.Include}; private={this.SuppressParent}";
	}
}