diff --git a/build/Shared/EqualityUtility.cs b/build/Shared/EqualityUtility.cs index 91fbbd114b0..140188697e8 100644 --- a/build/Shared/EqualityUtility.cs +++ b/build/Shared/EqualityUtility.cs @@ -74,6 +74,12 @@ internal static bool SetEqualWithNullCheck( return identityEquals; } + // Verify they could be equal by count + if (self.Count != other.Count) + { + return false; + } + if (comparer == null) { comparer = EqualityComparer.Default; @@ -99,6 +105,12 @@ internal static bool DictionaryEquals( return identityEquals; } + // Verify they could be equal by count + if (self.Count != other.Count) + { + return false; + } + if (!self.Keys.OrderedEquals( other.Keys, s => s, diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/Logging/TransitiveNoWarnUtils.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/Logging/TransitiveNoWarnUtils.cs index 1f317ebfd81..367afcfe852 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/Logging/TransitiveNoWarnUtils.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/Logging/TransitiveNoWarnUtils.cs @@ -83,7 +83,7 @@ private static PackageSpecificWarningProperties ExtractTransitiveNoWarnPropertie { var dependencyMapping = new Dictionary(StringComparer.OrdinalIgnoreCase); var queue = new Queue(); - var seen = new HashSet(); + var seen = new Dictionary>(StringComparer.OrdinalIgnoreCase); var resultWarningProperties = new PackageSpecificWarningProperties(); var packageNoWarn = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -147,7 +147,7 @@ private static PackageSpecificWarningProperties ExtractTransitiveNoWarnPropertie parentPackageSpecificNoWarn); // Add the parent project to the seen set to prevent adding it back to the queue - seen.Add(new DependencyNode(id: parentProjectName, + AddToSeen(seen, new DependencyNode(id: parentProjectName, isProject: true, projectWideNoWarn: parentProjectWideNoWarn, packageSpecificNoWarn: parentPackageSpecificNoWarn)); @@ -156,7 +156,11 @@ private static PackageSpecificWarningProperties ExtractTransitiveNoWarnPropertie while (queue.Count > 0) { var node = queue.Dequeue(); - if (seen.Add(node) && dependencyMapping.TryGetValue(node.Id, out var nodeLookUp)) + + // Check if the node has already been visited, or if the node is a NoWarn superset of + // an existing node. If this is a superset it will not provide any new paths where a + // warning should be shown. + if (AddToSeen(seen, node) && dependencyMapping.TryGetValue(node.Id, out var nodeLookUp)) { var nodeId = node.Id; var nodeIsProject = node.IsProject; @@ -262,6 +266,44 @@ private static WarningPropertiesCollection GetNodeWarningProperties( return collection; } + /// + /// Add to the seen list for tracking. + /// + /// True if the node should be walked + private static bool AddToSeen(Dictionary> seen, DependencyNode node) + { + var id = node.Id; + + if (!seen.TryGetValue(id, out var visitedNodes)) + { + // New id + visitedNodes = new HashSet() { node }; + seen.Add(id, visitedNodes); + return true; + } + + // Add the node for tracking, if a subset of this node + // has already been walked then skip it, it will not provide + // any new warning possibilities. + if (!visitedNodes.Add(node)) + { + var nodeProps = node.NodeWarningProperties; + + if (nodeProps != null) + { + foreach (var existingNode in visitedNodes) + { + if (nodeProps.IsSubSetOf(existingNode.NodeWarningProperties)) + { + return false; + } + } + } + } + + return true; + } + private static void AddDependenciesToQueue(IEnumerable dependencies, Queue queue, HashSet projectWideNoWarn, @@ -728,6 +770,79 @@ public bool Equals(NodeWarningProperties other) return EqualityUtility.SetEqualWithNullCheck(ProjectWide, other.ProjectWide) && EqualityUtility.DictionaryEquals(PackageSpecific, other.PackageSpecific, (s, o) => EqualityUtility.SetEqualWithNullCheck(s, o)); } + + /// + /// True if the given set is a subset of this set, or equal to it. + /// + /// Null is considered an empty set, and will return true. + public bool IsSubSetOf(NodeWarningProperties other) + { + if (other == null) + { + return true; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (IsSubSetOfWithNullCheck(ProjectWide, other.ProjectWide)) + { + var package = PackageSpecific; + var otherPackage = other.PackageSpecific; + + if (otherPackage == null || otherPackage.Count == 0) + { + return true; + } + + if (package == null || package.Count == 0) + { + return false; + } + + if (otherPackage.Count <= package.Count) + { + // To be subset this set of package specific warnings must contain + // every id and code found in other. A single miss will fail the check. + foreach (var pair in otherPackage) + { + if (!package.TryGetValue(pair.Key, out var codes) + || !codes.IsSubsetOf(pair.Value)) + { + return false; + } + } + } + + return true; + } + + return false; + } + + private static bool IsSubSetOfWithNullCheck(HashSet parent, HashSet other) + { + // Null is empty and always a subset. + if (other == null || other.Count == 0) + { + return true; + } + + // A null or empty parent cannot be a superset. + if (parent == null || parent.Count == 0) + { + return false; + } + + if (other.Count <= parent.Count) + { + return parent.IsSubsetOf(other); + } + + return false; + } } } } diff --git a/test/NuGet.Core.Tests/NuGet.Commands.Test/StandaloneProjectTests.cs b/test/NuGet.Core.Tests/NuGet.Commands.Test/StandaloneProjectTests.cs index f99ff3f9cf6..365c56f9723 100644 --- a/test/NuGet.Core.Tests/NuGet.Commands.Test/StandaloneProjectTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Commands.Test/StandaloneProjectTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; @@ -47,6 +47,7 @@ public async Task StandaloneProject_BasicRestore() spec.RestoreMetadata.OutputPath = Path.Combine(pathContext.SolutionRoot, "x"); spec.RestoreMetadata.ProjectUniqueName = "x"; spec.RestoreMetadata.ProjectName = "x"; + spec.RestoreMetadata.ProjectPath = Path.Combine(pathContext.SolutionRoot, "x.csproj"); dgFile.AddProject(spec); dgFile.AddRestore("x");