File: Rules\DependenciesGroupsForEachTFMRule.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// 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.Text;
using System.Xml.Linq;
using NuGet.Client;
using NuGet.Common;
using NuGet.ContentModel;
using NuGet.Frameworks;
using NuGet.Packaging.Core;
using NuGet.RuntimeModel;

namespace NuGet.Packaging.Rules
{
    internal class DependenciesGroupsForEachTFMRule : IPackageRule
    {
        private static readonly NuGetFramework Net00 = NuGetFramework.Parse("net00");

        public string MessageFormat => AnalysisResources.DependenciesGroupsForEachTFMHasNoExactMatch;

        private string CompatMatchFoundWarningMessageFormat => AnalysisResources.DependenciesGroupsForEachTFMHasCompatMatch;

        public IEnumerable<PackagingLogMessage> Validate(PackageArchiveReader package)
        {
            var files = package.GetFiles().
                Where(t => PathUtility.GetPathWithDirectorySeparator(t).Count(m => m == Path.DirectorySeparatorChar) > 1);
            var packageNuspec = ExtractTFMsFromNuspec(package.GetNuspec());
            var (compatNotExactMatches, noExactMatchesFromFile, noExactMatchesFromNuspec) = Categorize(files, packageNuspec);
            return GenerateWarnings(compatNotExactMatches, noExactMatchesFromFile, noExactMatchesFromNuspec);
        }

        internal IEnumerable<PackagingLogMessage> GenerateWarnings(HashSet<NuGetFramework> compatNotExactMatches, HashSet<NuGetFramework> noExactMatchesFromFile, HashSet<NuGetFramework> noExactMatchesFromNuspec)
        {
            (string noExactMatchString, string compatMatchString) = GenerateWarningString(noExactMatchesFromFile, noExactMatchesFromNuspec, compatNotExactMatches);

            var issues = new List<PackagingLogMessage>();

            if (noExactMatchesFromFile.Count != 0 || noExactMatchesFromNuspec.Count != 0)
            {
                issues.Add(PackagingLogMessage.CreateWarning(noExactMatchString, NuGetLogCode.NU5128));
            }

            if (compatNotExactMatches.Count != 0)
            {
                issues.Add(PackagingLogMessage.CreateWarning(compatMatchString, NuGetLogCode.NU5130));
            }

            if (issues.Count != 0)
            {
                return issues;
            }

            return Array.Empty<PackagingLogMessage>();
        }

        internal (HashSet<NuGetFramework>, HashSet<NuGetFramework>, HashSet<NuGetFramework>) Categorize(IEnumerable<string> files, IEnumerable<NuGetFramework> tfmsFromNuspec)
        {
            var managedCodeConventions = new ManagedCodeConventions(RuntimeGraph.Empty);
            Func<object, object, bool> isCompatible = managedCodeConventions.Properties["tfm"].CompatibilityTest;
            var collection = new ContentItemCollection();
            collection.Load(files);

            List<ContentItemGroup> libItems = new();
            List<ContentItemGroup> refItems = new();
            ContentExtractor.GetContentForPattern(collection, managedCodeConventions.Patterns.CompileLibAssemblies, libItems);
            ContentExtractor.GetContentForPattern(collection, managedCodeConventions.Patterns.CompileRefAssemblies, refItems);

            var tfmsFromFilesSet = new HashSet<NuGetFramework>();
            tfmsFromFilesSet.AddRange(ContentExtractor.GetGroupFrameworks(libItems));
            tfmsFromFilesSet.AddRange(ContentExtractor.GetGroupFrameworks(refItems));

            var tfmsFromFiles = tfmsFromFilesSet.Where(t => t != Net00).ToList();

            var noExactMatchesFromFile = new HashSet<NuGetFramework>();
            var noExactMatchesFromNuspec = new HashSet<NuGetFramework>();
            var compatNotExactMatches = new HashSet<NuGetFramework>();

            noExactMatchesFromFile.AddRange(tfmsFromFiles.Where(t => !tfmsFromNuspec.Contains(t)));
            noExactMatchesFromNuspec.AddRange(tfmsFromNuspec.Where(t => !tfmsFromFiles.Contains(t)));

            foreach (var fileTFM in noExactMatchesFromFile)
            {
                foreach (var nuspecTFM in noExactMatchesFromNuspec)
                {
                    if (isCompatible(fileTFM, nuspecTFM))
                    {
                        compatNotExactMatches.Add(fileTFM);
                        break;
                    }
                }
            }

            if (compatNotExactMatches.Count != 0)
            {
                noExactMatchesFromFile.RemoveWhere(p => compatNotExactMatches.Contains(p));
            }

            return (compatNotExactMatches, noExactMatchesFromFile, noExactMatchesFromNuspec);
        }

        internal (string noExactMatchString, string compatMatchString) GenerateWarningString(ICollection<NuGetFramework> noExactMatchesFromFile,
                ICollection<NuGetFramework> noExactMatchesFromNuspec, ICollection<NuGetFramework> compatNotExactMatches)
        {
            var noExactMatchString = new StringBuilder();
            var compatMatchString = new StringBuilder();

            if (noExactMatchesFromFile.Count != 0)
            {
                noExactMatchString.AppendLine(MessageFormat);

                foreach (var tfm in noExactMatchesFromFile)
                {
                    if (tfm == noExactMatchesFromFile.First())
                    {
                        noExactMatchString.Append(AnalysisResources.DependenciesGroupsForEachTFMBeginningToNuspec);
                        noExactMatchString.Append(" ");
                        noExactMatchString.Append(tfm.GetFrameworkString());
                        continue;
                    }
                    noExactMatchString.Append(" ");
                    noExactMatchString.AppendLine(AnalysisResources.DependenciesGroupsForEachTFMEndingToNuspec);

                    noExactMatchString.Append(AnalysisResources.DependenciesGroupsForEachTFMBeginningToNuspec);
                    noExactMatchString.Append(" ");
                    noExactMatchString.Append(tfm.GetFrameworkString());
                }
                noExactMatchString.Append(" ");
                noExactMatchString.AppendLine(AnalysisResources.DependenciesGroupsForEachTFMEndingToNuspec);
            }

            if (noExactMatchesFromNuspec.Count != 0)
            {
                if (noExactMatchString.Length == 0)
                {
                    noExactMatchString.AppendLine(MessageFormat);

                }

                foreach (var tfm in noExactMatchesFromNuspec)
                {
                    if (tfm == noExactMatchesFromNuspec.First())
                    {
                        noExactMatchString.Append(AnalysisResources.DependenciesGroupsForEachTFMBeginningToFiles);
                        noExactMatchString.Append(" ");
                        noExactMatchString.Append(tfm.GetShortFolderName());
                        continue;
                    }
                    noExactMatchString.Append(" ");
                    noExactMatchString.AppendLine(AnalysisResources.DependenciesGroupsForEachTFMEndingToFile);
                    noExactMatchString.Append(AnalysisResources.DependenciesGroupsForEachTFMBeginningToFiles);
                    noExactMatchString.Append(" ");
                    noExactMatchString.Append(tfm.GetShortFolderName());
                }
                noExactMatchString.Append(" ");
                noExactMatchString.Append(AnalysisResources.DependenciesGroupsForEachTFMEndingToFile);
            }

            if (compatNotExactMatches.Count != 0)
            {
                compatMatchString.AppendLine(CompatMatchFoundWarningMessageFormat);

                foreach (var tfm in compatNotExactMatches)
                {
                    if (tfm == compatNotExactMatches.First())
                    {
                        compatMatchString.Append(AnalysisResources.DependenciesGroupsForEachTFMBeginningToNuspec);
                        compatMatchString.Append(" ");
                        compatMatchString.Append(tfm.GetFrameworkString());
                        continue;
                    }
                    compatMatchString.Append(" ");
                    compatMatchString.AppendLine(AnalysisResources.DependenciesGroupsForEachTFMEndingToNuspec);
                    compatMatchString.Append(AnalysisResources.DependenciesGroupsForEachTFMBeginningToNuspec);
                    compatMatchString.Append(" ");
                    compatMatchString.Append(tfm.GetFrameworkString());
                }
                compatMatchString.Append(" ");
                compatMatchString.Append(AnalysisResources.DependenciesGroupsForEachTFMEndingToNuspec);
            }

            return (noExactMatchString.ToString().Trim(), compatMatchString.ToString());
        }

        private static IEnumerable<NuGetFramework> ExtractTFMsFromNuspec(Stream packageNuspecStream)
        {
            var packageNuspec = new NuspecReader(packageNuspecStream);
            var nuspec = packageNuspec.Xml;
            if (nuspec != null)
            {
                XNamespace name = nuspec.Root!.Name.Namespace;
                return nuspec.Descendants(XName.Get("{" + name.NamespaceName + "}dependencies")).Elements().Attributes("targetFramework").Select(f => NuGetFramework.Parse(f.Value));
            }
            return Array.Empty<NuGetFramework>();
        }
    }
}