// 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; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Versioning; using NuGet.Frameworks; using NuGet.Versioning; namespace Microsoft.Extensions.ProjectModel.Resolution { public class LibraryManager { private IList _libraries; private IList _diagnostics; private readonly object _initializeLock = new object(); private Dictionary> _inverse; private Dictionary _graph; private readonly string _projectPath; private readonly NuGetFramework _targetFramework; public LibraryManager(string projectPath, NuGetFramework targetFramework, IList libraries) { _projectPath = projectPath; _targetFramework = targetFramework; _libraries = libraries; } public void AddGlobalDiagnostics(DiagnosticMessage message) { if (_diagnostics == null) { _diagnostics = new List(); } _diagnostics.Add(message); } private Dictionary Graph { get { EnsureGraph(); return _graph; } } private Dictionary> InverseGraph { get { EnsureInverseGraph(); return _inverse; } } public IEnumerable GetReferencingLibraries(string name) { IEnumerable libraries; if (InverseGraph.TryGetValue(name, out libraries)) { return libraries; } return Enumerable.Empty(); } public LibraryDescription GetLibrary(string name) { LibraryDescription library; if (Graph.TryGetValue(name, out library)) { return library; } return null; } public IEnumerable GetLibraries() { EnsureGraph(); return _graph.Values; } public IList GetAllDiagnostics() { var messages = new List(); if (_diagnostics != null) { messages.AddRange(_diagnostics); } foreach (var library in GetLibraries()) { string projectPath = library.RequestedRange.SourceFilePath ?? _projectPath; if (!library.Resolved) { string message; string errorCode; if (library.Compatible) { errorCode = ErrorCodes.NU1001; message = $"The dependency {library.RequestedRange.Name} {library.RequestedRange.VersionRange} could not be resolved."; } else { errorCode = ErrorCodes.NU1002; var projectName = Directory.GetParent(_projectPath).Name; message = $"The dependency {library.Identity} in project {projectName} does not support framework {library.Framework}."; } messages.Add( new DiagnosticMessage( errorCode, message, projectPath, DiagnosticMessageSeverity.Error, library.RequestedRange.SourceLine, library.RequestedRange.SourceColumn, library)); } else { // Skip libraries that aren't specified in a project.json if (string.IsNullOrEmpty(library.RequestedRange.SourceFilePath)) { continue; } if (library.RequestedRange.VersionRange == null) { // TODO: Show errors/warnings for things without versions continue; } // If we ended up with a declared version that isn't what was asked for directly // then report a warning // Case 1: Non floating version and the minimum doesn't match what was specified // Case 2: Floating version that fell outside of the range if ((!library.RequestedRange.VersionRange.IsFloating && library.RequestedRange.VersionRange.MinVersion != library.Identity.Version) || (library.RequestedRange.VersionRange.IsFloating && !library.RequestedRange.VersionRange.EqualsFloating(library.Identity.Version))) { var message = string.Format("Dependency specified was {0} but ended up with {1}.", library.RequestedRange, library.Identity); messages.Add( new DiagnosticMessage( ErrorCodes.NU1007, message, projectPath, DiagnosticMessageSeverity.Warning, library.RequestedRange.SourceLine, library.RequestedRange.SourceColumn, library)); } } } return messages; } private void EnsureGraph() { lock (_initializeLock) { if (_graph == null) { _graph = _libraries.ToDictionary(l => l.Identity.Name, StringComparer.Ordinal); _libraries = null; } } } private void EnsureInverseGraph() { EnsureGraph(); lock (_initializeLock) { if (_inverse == null) { BuildInverseGraph(); } } } private void BuildInverseGraph() { var firstLevelLookups = new Dictionary>(StringComparer.OrdinalIgnoreCase); var visited = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in _graph.Values) { Visit(item, firstLevelLookups, visited); } _inverse = new Dictionary>(StringComparer.OrdinalIgnoreCase); // Flatten the graph foreach (var item in _graph.Values) { Flatten(item, firstLevelLookups: firstLevelLookups); } } private void Visit(LibraryDescription item, Dictionary> inverse, HashSet visited) { if (!visited.Add(item.Identity.Name)) { return; } foreach (var dependency in item.Dependencies) { List dependents; if (!inverse.TryGetValue(dependency.Name, out dependents)) { dependents = new List(); inverse[dependency.Name] = dependents; } dependents.Add(item); Visit(_graph[dependency.Name], inverse, visited); } } private void Flatten(LibraryDescription info, Dictionary> firstLevelLookups, HashSet parentDependents = null) { IEnumerable libraryDependents; if (!_inverse.TryGetValue(info.Identity.Name, out libraryDependents)) { List firstLevelDependents; if (firstLevelLookups.TryGetValue(info.Identity.Name, out firstLevelDependents)) { var allDependents = new HashSet(LibraryDescriptionComparer.Instance); foreach (var dependent in firstLevelDependents) { allDependents.Add(dependent); Flatten(dependent, firstLevelLookups, allDependents); } libraryDependents = allDependents; } else { libraryDependents = Enumerable.Empty(); } _inverse[info.Identity.Name] = libraryDependents; } AddRange(parentDependents, libraryDependents); } private static Func> GetLibraryInfoThunk(IEnumerable libraries) { return () => libraries; } private static void AddRange(HashSet source, IEnumerable values) { if (source != null) { foreach (var value in values) { source.Add(value); } } } private class LibraryDescriptionComparer : IEqualityComparer { public static readonly LibraryDescriptionComparer Instance = new LibraryDescriptionComparer(); private LibraryDescriptionComparer() { } public bool Equals(LibraryDescription x, LibraryDescription y) { return string.Equals(x.Identity.Name, y.Identity.Name, StringComparison.OrdinalIgnoreCase); } public int GetHashCode(LibraryDescription obj) { return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Identity.Name); } } } }