File: ProjectLockFile\PackagesLockFileUtilities.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.ProjectModel\NuGet.ProjectModel.csproj (NuGet.ProjectModel)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.ProjectModel.ProjectLockFile;
using NuGet.Shared;

namespace NuGet.ProjectModel
{
    public static class PackagesLockFileUtilities
    {
        public static bool IsNuGetLockFileEnabled(PackageSpec project)
        {
            if (project == null)
            {
                throw new ArgumentNullException(nameof(project));
            }
            var restorePackagesWithLockFile = project.RestoreMetadata?.RestoreLockProperties.RestorePackagesWithLockFile;
            return MSBuildStringUtility.IsTrue(restorePackagesWithLockFile) || File.Exists(GetNuGetLockFilePath(project));
        }

        public static string GetNuGetLockFilePath(PackageSpec project)
        {
            if (project.RestoreMetadata == null || project.BaseDirectory == null)
            {
                // RestoreMetadata or project BaseDirectory is not set which means it's probably called through test.
                return null;
            }

            var path = project.RestoreMetadata.RestoreLockProperties.NuGetLockFilePath;

            if (!string.IsNullOrEmpty(path))
            {
                return Path.Combine(project.BaseDirectory, path);
            }

            var projectName = Path.GetFileNameWithoutExtension(project.RestoreMetadata.ProjectPath);
            return GetNuGetLockFilePath(project.BaseDirectory, projectName);
        }

        public static string GetNuGetLockFilePath(string baseDirectory, string projectName)
        {
            if (!string.IsNullOrEmpty(projectName))
            {
                var path = Path.Combine(baseDirectory, "packages." + projectName.Replace(' ', '_') + ".lock.json");

                if (File.Exists(path))
                {
                    return path;
                }
            }

            return Path.Combine(baseDirectory, PackagesLockFileFormat.LockFileName);
        }

        /// <summary>
        /// The lock file will get invalidated if one or more of the below are true
        ///     1. The target frameworks list of the current project was updated.
        ///     2. The runtime list of the current project waw updated.
        ///     3. The packages of the current project were updated.
        ///     4. The packages of the dependent projects were updated.
        ///     5. The framework list of the dependent projects were updated with frameworks incompatible with the main project framework.
        ///     6. If the version of the <paramref name="nuGetLockFile"/> is larger than the current tools <see cref="PackagesLockFileFormat.PackagesLockFileVersion"/>.
        /// </summary>
        /// <param name="dgSpec">The <see cref="DependencyGraphSpec"/> for the new project defintion.</param>
        /// <param name="nuGetLockFile">The current <see cref="PackagesLockFile"/>.</param>
        /// <returns>Returns LockFileValidityWithInvalidReasons object with IsValid set to true if the lock file is valid false otherwise.
        /// The second return type is a localized message that indicates in further detail the reason for the inconsistency.</returns>
        public static LockFileValidationResult IsLockFileValid(DependencyGraphSpec dgSpec, PackagesLockFile nuGetLockFile)
        {
            if (dgSpec == null)
                throw new ArgumentNullException(nameof(dgSpec));

            if (nuGetLockFile == null)
                throw new ArgumentNullException(nameof(nuGetLockFile));

            List<string> invalidReasons = new List<string>();

            // Current tools know how to read only previous formats including the current
            if (PackagesLockFileFormat.PackagesLockFileVersion < nuGetLockFile.Version)
            {
                invalidReasons.Add(string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.PackagesLockFile_IncompatibleLockFileVersion,
                            PackagesLockFileFormat.PackagesLockFileVersion
                            ));

                return new LockFileValidationResult(false, invalidReasons);
            }

            bool useAliasForMessages = nuGetLockFile.Version == 3;

            var uniqueName = dgSpec.Restore.First();
            var project = dgSpec.GetProjectSpec(uniqueName);

            // Validate all the direct dependencies
            NuGetFramework[] lockFileFrameworks = nuGetLockFile.Targets
                .Where(t => t.TargetFramework != null && string.IsNullOrEmpty(t.RuntimeIdentifier))
                .Select(t => t.TargetFramework)
                .ToArray();

            if (project.TargetFrameworks.Count != lockFileFrameworks.Length)
            {
                invalidReasons.Add(string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.PackagesLockFile_MismatchedTargetFrameworks,
                            string.Join(",", project.TargetFrameworks.Select(e => e.FrameworkName.GetShortFolderName())),
                            string.Join(",", lockFileFrameworks.Select(e => e.GetShortFolderName()))
                            ));
            }
            else
            {
                // Validate the runtimes for the current project did not change.
                var projectRuntimesKeys = project.RuntimeGraph.Runtimes.Select(r => r.Key).Where(k => k != null);
                var lockFileRuntimes = nuGetLockFile.Targets.Select(t => t.RuntimeIdentifier).Where(r => r != null).Distinct();

                if (!projectRuntimesKeys.OrderedEquals(
                            lockFileRuntimes,
                            x => x,
                            StringComparer.InvariantCultureIgnoreCase,
                            StringComparer.InvariantCultureIgnoreCase))
                {
                    invalidReasons.Add(string.Format(
                                    CultureInfo.CurrentCulture,
                                    Strings.PackagesLockFile_RuntimeIdentifiersChanged,
                                    string.Join(";", projectRuntimesKeys.OrderBy(e => e)),
                                    string.Join(";", lockFileRuntimes.OrderBy(e => e))
                                    ));
                }

                foreach (var framework in project.TargetFrameworks)
                {
                    PackagesLockFileTarget target = GetTargetForTargetFrameworkInformation(nuGetLockFile, framework.FrameworkName, framework.TargetAlias);

                    if (target == null)
                    {
                        // a new target found in the dgSpec so invalidate existing lock file.
                        invalidReasons.Add(string.Format(
                                    CultureInfo.CurrentCulture,
                                    Strings.PackagesLockFile_NewTargetFramework,
                                    useAliasForMessages ? framework.TargetAlias : framework.FrameworkName.GetShortFolderName())
                                );

                        continue;
                    }

                    IEnumerable<LockFileDependency> directDependencies = target.Dependencies.Where(dep => dep.Type == PackageDependencyType.Direct);

                    (var hasProjectDependencyChanged, var pmessage) = HasDirectPackageDependencyChanged(framework.Dependencies, directDependencies, useAliasForMessages ? framework.TargetAlias : target.TargetFramework.GetShortFolderName());
                    if (hasProjectDependencyChanged)
                    {
                        // lock file is out of sync
                        invalidReasons.Add(pmessage);
                    }

                    var transitiveDependenciesEnforcedByCentralVersions = target.Dependencies.Where(dep => dep.Type == PackageDependencyType.CentralTransitive).ToList();
                    var transitiveDependencies = target.Dependencies.Where(dep => dep.Type == PackageDependencyType.Transitive).ToList();

                    (var hasTransitiveDependencyChanged, var tmessage) = HasProjectTransitiveDependencyChanged(framework.CentralPackageVersions, transitiveDependenciesEnforcedByCentralVersions, transitiveDependencies);
                    if (hasTransitiveDependencyChanged)
                    {
                        // lock file is out of sync
                        invalidReasons.Add(tmessage);
                    }
                }

                // Validate all P2P references
                foreach (var restoreMetadataFramework in project.RestoreMetadata.TargetFrameworks)
                {
                    var target = GetTargetForTargetFrameworkInformation(nuGetLockFile, restoreMetadataFramework.FrameworkName, restoreMetadataFramework.TargetAlias);
                    var targetFrameworkInformation = project.TargetFrameworks.FirstOrDefault(e => e.TargetAlias == restoreMetadataFramework.TargetAlias);

                    if (target == null)
                        continue;

                    var queue = new Queue<Tuple<string, string>>();
                    var visitedP2PReference = new HashSet<string>();

                    foreach (var projectReference in restoreMetadataFramework.ProjectReferences)
                    {
                        if (visitedP2PReference.Add(projectReference.ProjectUniqueName))
                        {
                            PackageSpec spec = dgSpec.GetProjectSpec(projectReference.ProjectUniqueName);
                            queue.Enqueue(new Tuple<string, string>(spec.Name, projectReference.ProjectUniqueName));

                            while (queue.Count > 0)
                            {
                                var projectNames = queue.Dequeue();
                                var p2pUniqueName = projectNames.Item2;
                                var p2pProjectName = projectNames.Item1;
                                var projectDependency = target.Dependencies.FirstOrDefault(
                                    dep => dep.Type == PackageDependencyType.Project &&
                                    StringComparer.OrdinalIgnoreCase.Equals(dep.Id, p2pProjectName));

                                if (projectDependency == null)
                                {
                                    // new direct project dependency.
                                    // If there are changes in the P2P2P references, they will be caught in HasP2PDependencyChanged.
                                    invalidReasons.Add(string.Format(
                                            CultureInfo.CurrentCulture,
                                            Strings.PackagesLockFile_ProjectReferenceAdded,
                                            p2pProjectName,
                                            useAliasForMessages ? target.TargetAlias : target.TargetFramework.GetShortFolderName()
                                            ));

                                    continue;
                                }

                                var p2pSpec = dgSpec.GetProjectSpec(p2pUniqueName);

                                if (p2pSpec != null)
                                {
                                    TargetFrameworkInformation p2pSpecTargetFrameworkInformation = default;
                                    if (p2pSpec.RestoreMetadata.ProjectStyle == ProjectStyle.PackagesConfig || p2pSpec.RestoreMetadata.ProjectStyle == ProjectStyle.Unknown)
                                    {
                                        // Skip compat check and dependency check for non PR projects.
                                        // Projects that are not PR do not undergo compat checks by NuGet and do not contribute anything transitively.
                                        p2pSpecTargetFrameworkInformation = p2pSpec.TargetFrameworks.FirstOrDefault();
                                    }
                                    else
                                    {
                                        p2pSpecTargetFrameworkInformation = p2pSpec.GetNearestTargetFramework(targetFrameworkInformation.FrameworkName, targetFrameworkInformation.TargetAlias);
                                        if (p2pSpecTargetFrameworkInformation.FrameworkName == null)
                                        {
                                            if (targetFrameworkInformation.FrameworkName is AssetTargetFallbackFramework atfFramework)
                                            {
                                                p2pSpecTargetFrameworkInformation = p2pSpec.GetNearestTargetFramework(atfFramework.AsFallbackFramework(), targetFrameworkInformation.TargetAlias);
                                            }
                                        }
                                    }
                                    // No compatible framework found
                                    if (p2pSpecTargetFrameworkInformation != null && p2pSpecTargetFrameworkInformation.FrameworkName != null)
                                    {
                                        // Get it based on the matching alias. The appropriate target framework information has already been calculated.
                                        var p2pSpecProjectRestoreMetadataFrameworkInfo = p2pSpec.RestoreMetadata.TargetFrameworks.FirstOrDefault(e => e.TargetAlias == p2pSpecTargetFrameworkInformation.TargetAlias);

                                        if (p2pSpecProjectRestoreMetadataFrameworkInfo != null)
                                        {
                                            (var hasChanged, var message) = HasP2PDependencyChanged(p2pSpecTargetFrameworkInformation.Dependencies, p2pSpecProjectRestoreMetadataFrameworkInfo.ProjectReferences, p2pSpecTargetFrameworkInformation.PackagesToPrune, targetFrameworkInformation.PackagesToPrune, projectDependency, dgSpec);

                                            if (hasChanged)
                                            {
                                                // P2P transitive package dependencies have changed                                            
                                                invalidReasons.Add(message);
                                            }

                                            foreach (var reference in p2pSpecProjectRestoreMetadataFrameworkInfo.ProjectReferences)
                                            {
                                                // Do not add private assets for processing.
                                                if (visitedP2PReference.Add(reference.ProjectUniqueName) && reference.PrivateAssets != LibraryIncludeFlags.All)
                                                {
                                                    var referenceSpec = dgSpec.GetProjectSpec(reference.ProjectUniqueName);
                                                    queue.Enqueue(new Tuple<string, string>(referenceSpec.Name, reference.ProjectUniqueName));
                                                }
                                            }
                                        }
                                        else // This should never happen.
                                        {
                                            throw new Exception(string.Format(CultureInfo.CurrentCulture, Strings.PackagesLockFile_RestoreMetadataMissingTfms));
                                        }
                                    }
                                    else
                                    {
                                        invalidReasons.Add(string.Format(
                                               CultureInfo.CurrentCulture,
                                               Strings.PackagesLockFile_ProjectReferenceHasNoCompatibleTargetFramework,
                                               p2pProjectName,
                                               useAliasForMessages ? restoreMetadataFramework.TargetAlias : restoreMetadataFramework.FrameworkName.GetShortFolderName()
                                               ));
                                    }
                                }
                                else // This can't happen. When adding the queue, the referenceSpec HAS to be discovered. If the project is otherwise missing, it will be discovered in HasP2PDependencyChanged
                                {
                                    throw new Exception(string.Format(
                                        CultureInfo.CurrentCulture,
                                        Strings.PackagesLockFile_UnableToLoadPackagespec,
                                        p2pUniqueName));
                                }
                            }
                        }
                    }
                }
            }

            bool isLockFileValid = invalidReasons.Count == 0;

            return new LockFileValidationResult(isLockFileValid, invalidReasons);

            static PackagesLockFileTarget GetTargetForTargetFrameworkInformation(PackagesLockFile nuGetLockFile, NuGetFramework framework, string targetAlias)
            {
                foreach (var target in nuGetLockFile.Targets.NoAllocEnumerate())
                {
                    if (EqualityUtility.EqualsWithNullCheck(target.TargetFramework, framework) &&
                        (nuGetLockFile.Version != 3 || StringComparer.OrdinalIgnoreCase.Equals(target.TargetAlias, targetAlias)))
                    {
                        return target;
                    }
                }
                return null;
            }
        }

        /// <summary>Compares two lock files to check if the structure is the same (all values are the same, other
        /// than SHA hash), and matches dependencies so the caller can easily compare SHA hashes.</summary>
        /// <param name="expected">The expected lock file structure. Usuaully generated from the project.</param>
        /// <param name="actual">The lock file that was loaded from the file on disk.</param>
        /// <returns>A <see cref="LockFileValidityWithMatchedResults"/>.</returns>
        public static LockFileValidityWithMatchedResults IsLockFileStillValid(PackagesLockFile expected, PackagesLockFile actual)
        {
            if (expected == null)
            {
                throw new ArgumentNullException(nameof(expected));
            }
            if (actual == null)
            {
                throw new ArgumentNullException(nameof(actual));
            }

            // do quick checks for obvious structure differences
            if (expected.Version != actual.Version)
            {
                return LockFileValidityWithMatchedResults.Invalid;
            }

            if (expected.Targets.Count != actual.Targets.Count)
            {
                return LockFileValidityWithMatchedResults.Invalid;
            }

            foreach (var expectedTarget in expected.Targets)
            {
                PackagesLockFileTarget actualTarget = null;

                for (var i = 0; i < actual.Targets.Count; i++)
                {
                    // Match by framework and alias (for v3) or just framework (for v1/v2)
                    if (actual.Targets[i].TargetFramework == expectedTarget.TargetFramework &&
                        StringComparer.OrdinalIgnoreCase.Equals(actual.Targets[i].TargetAlias, expectedTarget.TargetAlias) &&
                        StringComparer.Ordinal.Equals(actual.Targets[i].RuntimeIdentifier, expectedTarget.RuntimeIdentifier))
                    {
                        if (actualTarget == null)
                        {
                            actualTarget = actual.Targets[i];
                        }
                        else
                        {
                            // more than 1? possible bug or bad hand edited lock file.
                            return LockFileValidityWithMatchedResults.Invalid;
                        }
                    }
                }

                if (actualTarget == null)
                {
                    return LockFileValidityWithMatchedResults.Invalid;
                }

                if (actualTarget.Dependencies.Count != expectedTarget.Dependencies.Count)
                {
                    return LockFileValidityWithMatchedResults.Invalid;
                }
            }

            // no obvious structure difference, so start trying to match individual dependencies
            var matchedDependencies = new List<KeyValuePair<LockFileDependency, LockFileDependency>>();
            var isLockFileStillValid = true;
            var dependencyComparer = LockFileDependencyComparerWithoutContentHash.Default;

            foreach (PackagesLockFileTarget expectedTarget in expected.Targets)
            {
                PackagesLockFileTarget actualTarget = actual.Targets.Single(t =>
                    t.TargetFramework == expectedTarget.TargetFramework &&
                    StringComparer.OrdinalIgnoreCase.Equals(t.TargetAlias, expectedTarget.TargetAlias) &&
                    StringComparer.Ordinal.Equals(t.RuntimeIdentifier, expectedTarget.RuntimeIdentifier));

                // Duplicate dependencies list so we can remove matches to validate that all dependencies were matched
                var actualDependencies = new Dictionary<LockFileDependency, LockFileDependency>(
                    actualTarget.Dependencies.Count,
                    dependencyComparer);
                foreach (LockFileDependency actualDependency in actualTarget.Dependencies)
                {
                    actualDependencies.Add(actualDependency, actualDependency);
                }

                foreach (LockFileDependency expectedDependency in expectedTarget.Dependencies)
                {
                    if (actualDependencies.TryGetValue(expectedDependency, out var actualDependency))
                    {
                        matchedDependencies.Add(new KeyValuePair<LockFileDependency, LockFileDependency>(expectedDependency, actualDependency));
                        actualDependencies.Remove(actualDependency);
                    }
                    else
                    {
                        return LockFileValidityWithMatchedResults.Invalid;
                    }
                }

                if (actualDependencies.Count != 0)
                {
                    return LockFileValidityWithMatchedResults.Invalid;
                }
            }

            return new LockFileValidityWithMatchedResults(isLockFileStillValid, matchedDependencies);
        }

        private static (bool, string) HasDirectPackageDependencyChanged(IEnumerable<LibraryDependency> newDependencies, IEnumerable<LockFileDependency> lockFileDependencies, string frameworkName)
        {
            // If the count is not the same, something has changed.
            // Otherwise the N^2 walk below determines whether anything has changed.
            var newPackageDependencies = newDependencies.Where(dep => dep.LibraryRange.TypeConstraint == LibraryDependencyTarget.Package);

            var newPackageDependenciesCount = newPackageDependencies.Count();
            var lockFileDependenciesCount = lockFileDependencies.Count();

            if (newPackageDependenciesCount != lockFileDependenciesCount)
            {
                return (true,
                           string.Format(
                               CultureInfo.CurrentCulture,
                               Strings.PackagesLockFile_PackageReferencesHaveChanged,
                               frameworkName,
                               lockFileDependenciesCount > 0 ? string.Join(", ", lockFileDependencies.Select(e => e.Id + ":" + e.RequestedVersion.ToNormalizedString()).OrderBy(dep => dep)) : Strings.None,
                               newPackageDependenciesCount > 0 ? string.Join(", ", newPackageDependencies.Select(e => e.LibraryRange.ToLockFileDependencyGroupString()).OrderBy(dep => dep)) : Strings.None)
                           );
            }

            foreach (var dependency in newPackageDependencies)
            {
                var lockFileDependency = lockFileDependencies.FirstOrDefault(d => StringComparer.OrdinalIgnoreCase.Equals(d.Id, dependency.Name));

                if (lockFileDependency == null)
                {
                    // dependency has changed and lock file is out of sync.
                    return (true,
                               string.Format(
                                   CultureInfo.CurrentCulture,
                                   Strings.PackagesLockFile_PackageReferenceAdded,
                                   dependency.Name,
                                   frameworkName)
                            );
                }
                if (!EqualityUtility.EqualsWithNullCheck(lockFileDependency.RequestedVersion, dependency.LibraryRange.VersionRange))
                {
                    // dependency has changed and lock file is out of sync.
                    return (true,
                               string.Format(
                                   CultureInfo.CurrentCulture,
                                   Strings.PackagesLockFile_PackageReferenceVersionChanged,
                                   dependency.Name,
                                   lockFileDependency.RequestedVersion.ToNormalizedString(),
                                   dependency.LibraryRange.VersionRange.ToNormalizedString())
                            );
                }
            }

            // no dependency changed. Lock file is still valid.
            return (false, string.Empty);
        }

        private static (bool, string) HasP2PDependencyChanged(IEnumerable<LibraryDependency> newDependencies, IEnumerable<ProjectRestoreReference> projectRestoreReferences, IReadOnlyDictionary<string, PrunePackageReference> dependentProjectPackagesToPrune, IReadOnlyDictionary<string, PrunePackageReference> packagesToPrune, LockFileDependency projectDependency, DependencyGraphSpec dgSpec)
        {
            // If the count is not the same, something has changed.
            // Otherwise we N^2 walk below determines whether anything has changed.
            var transitivelyFlowingDependencies = newDependencies.Where(
                dep => dep.LibraryRange.TypeConstraint == LibraryDependencyTarget.Package
                    && dep.SuppressParent != LibraryIncludeFlags.All
                    && !IsDependencyPruned(dep, dependentProjectPackagesToPrune));

            var transitivelyFlowingProjectReferences = projectRestoreReferences.Where(e => e.PrivateAssets != LibraryIncludeFlags.All);

            var transitiveDependencies = transitivelyFlowingDependencies.Count() + transitivelyFlowingProjectReferences.Count();

            // If count is not the same, then something has changed.
            // When pruning, the lock file may have more dependencies than what gets resolved for the project.
            if (transitiveDependencies != projectDependency.Dependencies.Count && packagesToPrune?.Count == 0)
            {
                return (true,
                        string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.PackagesLockFile_ProjectReferencesHasChange,
                            projectDependency.Id,
                            transitiveDependencies > 0 ? string.Join(",", transitivelyFlowingDependencies.Select(dep => dep.Name).Concat(projectRestoreReferences.Select(dep => dep.ProjectUniqueName)).OrderBy(dep => dep)) : Strings.None,
                            projectDependency.Dependencies.Count > 0 ? string.Join(",", projectDependency.Dependencies.Select(dep => dep.Id).OrderBy(dep => dep)) : Strings.None
                            )
                        );
            }

            foreach (var dependency in transitivelyFlowingDependencies)
            {
                var matchedP2PLibrary = projectDependency.Dependencies.FirstOrDefault(dep => StringComparer.OrdinalIgnoreCase.Equals(dep.Id, dependency.Name));

                if (matchedP2PLibrary == null && IsDependencyPruned(dependency, packagesToPrune))
                {
                    // This dependency is pruned and a pre-pruning lock file containing the dependency is still valid.
                    continue;
                }

                if (matchedP2PLibrary == null || !EqualityUtility.EqualsWithNullCheck(matchedP2PLibrary.VersionRange, dependency.LibraryRange.VersionRange))
                {
                    // P2P dependency has changed and lock file is out of sync.
                    return (true,
                        string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.PackagesLockFile_ProjectReferenceDependenciesHasChanged,
                            projectDependency.Id
                            )
                        );
                }
            }

            foreach (var dependency in transitivelyFlowingProjectReferences)
            {
                var referenceSpec = dgSpec.GetProjectSpec(dependency.ProjectUniqueName);
                var matchedP2PLibrary = projectDependency.Dependencies.FirstOrDefault(dep => StringComparer.OrdinalIgnoreCase.Equals(dep.Id, referenceSpec.Name));

                if (matchedP2PLibrary == null) // Do not check the version for the projects, or else https://github.com/nuget/home/issues/7935
                {
                    // P2P dependency has changed and lock file is out of sync.
                    return (true,
                        string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.PackagesLockFile_ProjectReferenceDependenciesHasChanged,
                            projectDependency.Id
                            )
                        );
                }
            }

            // no dependency changed. Lock file is still valid.
            return (false, string.Empty);

            static bool IsDependencyPruned(LibraryDependency dependency, IReadOnlyDictionary<string, PrunePackageReference> packagesToPrune)
            {
                if (packagesToPrune?.TryGetValue(dependency.Name, out PrunePackageReference packageToPrune) == true
                    && dependency.LibraryRange.VersionRange.Satisfies(packageToPrune.VersionRange.MaxVersion))
                {
                    return true;
                }
                return false;
            }
        }

        /// <summary>
        /// The method will return true if:
        /// 1. If a transitive dependency from the lock file is now added to the central file.
        ///     or
        /// 1. If there is a mistmatch between the RequestedVersion of a lock file dependency marked as CentralTransitive and the the version specified in the central package management file.
        ///     or
        /// 2. If a central version that is a transitive dependency is removed from CPVM the lock file is invalidated.
        /// </summary>
        private static (bool, string) HasProjectTransitiveDependencyChanged(
            IReadOnlyDictionary<string, CentralPackageVersion> centralPackageVersions,
            IList<LockFileDependency> lockFileCentralTransitiveDependencies,
            IList<LockFileDependency> lockTransitiveDependencies)
        {
            // Transitive dependencies moved to be centraly managed will invalidate the lock file
            LockFileDependency dependency = lockTransitiveDependencies.FirstOrDefault(dep => centralPackageVersions.ContainsKey(dep.Id));

            if (dependency != null)
            {
                return (true,
                        string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.PackagesLockFile_ProjectTransitiveDependencyChanged,
                            dependency.Id
                            )
                        );
            }

            foreach (var lockFileDependencyEnforcedByCPV in lockFileCentralTransitiveDependencies)
            {
                if (centralPackageVersions.TryGetValue(lockFileDependencyEnforcedByCPV.Id, out var centralPackageVersion))
                {
                    if (centralPackageVersion != null && !EqualityUtility.EqualsWithNullCheck(lockFileDependencyEnforcedByCPV.RequestedVersion, centralPackageVersion.VersionRange))
                    {
                        return (true,
                                string.Format(
                                    CultureInfo.CurrentCulture,
                                    Strings.PackagesLockFile_ProjectTransitiveDependencyVersionChanged,
                                    lockFileDependencyEnforcedByCPV.RequestedVersion.ToNormalizedString(),
                                    centralPackageVersion.VersionRange.ToNormalizedString()
                                    )
                                );
                    }
                    continue;
                }

                // The central version was removed
                return (true,
                        string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.PackagesLockFile_CentralPackageVersionRemoved,
                            lockFileDependencyEnforcedByCPV.Id
                            )
                        );
            }

            return (false, string.Empty);
        }

        /// <summary>
        /// A class to return information about lock file validity
        /// </summary>
        public class LockFileValidityWithMatchedResults
        {
            /// <summary>
            /// True if the lock file had the expected structure (all values expected, other than content hash)
            /// </summary>
            public bool IsValid { get; }

            /// <summary>
            /// A list of matched dependencies, so content sha can easily be checked.
            /// </summary>
            public IReadOnlyList<KeyValuePair<LockFileDependency, LockFileDependency>> MatchedDependencies { get; }

            public LockFileValidityWithMatchedResults(bool isValid, IReadOnlyList<KeyValuePair<LockFileDependency, LockFileDependency>> matchedDependencies)
            {
                IsValid = isValid;
                MatchedDependencies = matchedDependencies;
            }

            public static readonly LockFileValidityWithMatchedResults Invalid =
                new LockFileValidityWithMatchedResults(isValid: false, matchedDependencies: null);
        }
    }

    /// <summary>
    /// A class to return information about lock file validity with invalid reasons.
    /// </summary>
    public class LockFileValidationResult
    {
        /// <summary>
        /// True if the packages.lock.json file dependencies match project.assets.json file dependencies
        /// </summary>
        public bool IsValid { get; }

        /// <summary>
        /// A list of reasons why lock file is invalid
        /// </summary>
        public IReadOnlyList<string> InvalidReasons { get; }

        public LockFileValidationResult(bool isValid, IReadOnlyList<string> invalidReasons)
        {
            IsValid = isValid;
            InvalidReasons = invalidReasons;
        }
    }
}