diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeWantToGetDependenciesViaDesignTimeBuild.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeWantToGetDependenciesViaDesignTimeBuild.cs index bcc395e48d64..5a97538139b3 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeWantToGetDependenciesViaDesignTimeBuild.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeWantToGetDependenciesViaDesignTimeBuild.cs @@ -37,10 +37,10 @@ public void ItShouldIgnoreAllDependenciesWithTypeNotEqualToPackageOrUnresolved() Assert.Equal(2, task.PackageDependenciesDesignTime.Count()); - // Verify only + // Verify only // top.package1 is type 'package' // top.package2 is type 'unresolved' - // + // // top.package3 is type 'unknown'. Should not appear in the list var item1 = task.PackageDependenciesDesignTime[0]; Assert.Equal("top.package1/1.0.0", item1.ItemSpec); @@ -372,14 +372,38 @@ public void ItShouldOnlyReturnTopLevelPackages() var item1 = task.PackageDependenciesDesignTime[0]; Assert.Equal("top.package1/1.0.0", item1.ItemSpec); + Assert.Equal("Error", item1.GetMetadata("DiagnosticLevel")); var item2 = task.PackageDependenciesDesignTime[1]; Assert.Equal("top.package2/1.0.0", item2.ItemSpec); + Assert.Equal(string.Empty, item2.GetMetadata("DiagnosticLevel")); var item3 = task.PackageDependenciesDesignTime[2]; Assert.Equal("top.package3/1.0.0", item3.ItemSpec); + Assert.Equal("Warning", item3.GetMetadata("DiagnosticLevel")); } + /// + /// Create a NuGet assets file for a project with 3 direct references, and 3 transitive references. + /// + /// Path to the NuGet global packages folder that the assets file will refer to. + /// Whether "top.package2" is a "package" (default) or "project" reference. + /// Whether "top.package2" is a "package" (default) or "project" reference. + /// The JSON string representing the project. + /// + /// The reference structure is as follows:
+ /// proj
+ /// + top.package1
+ /// | + dependent.package1
+ /// | | + dependent.package3
+ /// | + dependent.package2
+ /// + top.package2
+ /// + top.package2
+ /// - + dependent.package1
+ /// - - + dependent.package3
+ ///
+ /// dependent.package2 has an error message, and dependent.package3 has a warning. + ///
private string CreateBasicProjectAssetsFile(string testRoot, string package2Type = "package", string package3Type = "package") { var json = @@ -587,7 +611,29 @@ private string CreateBasicProjectAssetsFile(string testRoot, string package2Type } } } - } + }, + "logs": [ + { + "code": "NU1001", + "level": "Error", + "warningLevel": 2, + "message": "some warning message", + "libraryId": "dependent.package2", + "targetGraphs": [ + "net6.0" + ] + }, + { + "code": "NU1002", + "level": "Warning", + "warningLevel": 1, + "message": "some warning message", + "libraryId": "dependent.package3", + "targetGraphs": [ + "net6.0" + ] + } + ] } """; return json.Replace("PACKAGE2_TYPE", package2Type) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageAssets.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageAssets.cs index 364d9b04a7f5..e1e9fcf1b39d 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageAssets.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageAssets.cs @@ -1438,6 +1438,7 @@ private void WritePackageDependenciesDesignTime() // Scan PackageDependencies to build the set of packages in our target. var allowItemSpecs = GetPackageDependencies(); + var diagnosticLevels = GetPackageDiagnosticLevels(); foreach (var package in _lockFile.Libraries) { @@ -1477,8 +1478,11 @@ private void WritePackageDependenciesDesignTime() : itemPath) ?? string.Empty; WriteMetadata(MetadataKeys.Path, path); - string itemDiagnosticLevel = GetPackageDiagnosticLevel(package); - var diagnosticLevel = itemDiagnosticLevel ?? string.Empty; + string diagnosticLevel = string.Empty; + if (diagnosticLevels?.TryGetValue(package.Name, out LogLevel level) ?? false) + { + diagnosticLevel = level.ToString(); + } WriteMetadata(MetadataKeys.DiagnosticLevel, diagnosticLevel); } } @@ -1506,28 +1510,92 @@ HashSet GetPackageDependencies() static string GetPackageId(LockFileTargetLibrary package) => $"{package.Name}/{package.Version.ToNormalizedString()}"; - string GetPackageDiagnosticLevel(LockFileLibrary package) + Dictionary GetPackageDiagnosticLevels() { - string target = _task.TargetFramework ?? ""; + if (_lockFile.LogMessages.Count == 0) + { + return null; + } + + var packageReverseDependencies = GetReverseDependencies(); + var result = new Dictionary(); + for (int i = 0; i < _lockFile.LogMessages.Count; i++) + { + var message = _lockFile.LogMessages[i]; + if (string.IsNullOrEmpty(message.LibraryId)) continue; - var messages = _lockFile.LogMessages.Where(log => - log.LibraryId == package.Name && - log.TargetGraphs.Any(tg => + if (message.TargetGraphs is null || message.TargetGraphs.Count == 0 || message.TargetGraphs.Any(ForCurrentTargetFramework)) { - var parts = tg.Split(LockFile.DirectorySeparatorChar); - var parsedTargetGraph = NuGetFramework.Parse(parts[0]); - var alias = _lockFile.PackageSpec.TargetFrameworks - .FirstOrDefault(tf => tf.FrameworkName == parsedTargetGraph) - ?.TargetAlias ?? tg; - return alias == target; - })); - - if (!messages.Any()) + ApplyDiagnosticLevel(message.LibraryId, message.Level, result, packageReverseDependencies); + } + } + + return result; + + Dictionary> GetReverseDependencies() { - return string.Empty; + var packageReverseDependencies = new Dictionary>(_compileTimeTarget.Libraries.Count, StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < _compileTimeTarget.Libraries.Count; i++) + { + var parentPackage = _compileTimeTarget.Libraries[i]; + if (string.IsNullOrEmpty(parentPackage.Name)) continue; + + if (!packageReverseDependencies.ContainsKey(parentPackage.Name)) + { + packageReverseDependencies[parentPackage.Name] = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + for (int j = 0; j < parentPackage.Dependencies.Count; j++) + { + var dependency = parentPackage.Dependencies[j].Id; + + if (!packageReverseDependencies.TryGetValue(dependency, out HashSet parentPackages)) + { + parentPackages = new HashSet(StringComparer.OrdinalIgnoreCase); + packageReverseDependencies[dependency] = parentPackages; + } + + parentPackages.Add(parentPackage.Name); + } + } + return packageReverseDependencies; + } + + bool ForCurrentTargetFramework(string targetFramework) + { + var parts = targetFramework.Split(LockFile.DirectorySeparatorChar); + var parsedTargetGraph = NuGetFramework.Parse(parts[0]); + var alias = _lockFile.PackageSpec.TargetFrameworks + .FirstOrDefault(tf => tf.FrameworkName == parsedTargetGraph) + ?.TargetAlias ?? targetFramework; + return alias == _task.TargetFramework; } - return messages.Max(log => log.Level).ToString(); + void ApplyDiagnosticLevel(string package, LogLevel messageLevel, Dictionary diagnosticLevels, Dictionary> reverseDependencies) + { + if (!reverseDependencies.TryGetValue(package, out HashSet parentPackages)) + { + // The package is not used in the current TargetFramework + return; + } + + if (diagnosticLevels.TryGetValue(package, out LogLevel cachedLevel)) + { + // Only continue if we need to increase the level + if (cachedLevel >= messageLevel) + { + return; + } + } + + diagnosticLevels[package] = messageLevel; + + // Flow changes upwards, towards the direct PackageReference + foreach (var parentPackage in parentPackages) + { + ApplyDiagnosticLevel(parentPackage, messageLevel, diagnosticLevels, reverseDependencies); + } + } } static DependencyType GetDependencyType(string dependencyTypeString)