File: Rules\ReferencesInNuspecMatchRefAssetsRule.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.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using NuGet.Common;
using NuGet.Frameworks;

namespace NuGet.Packaging.Rules
{
    internal class ReferencesInNuspecMatchRefAssetsRule : IPackageRule
    {
        private string _addToRefFormat = AnalysisResources.ReferencesInNuspecAndRefFilesDontMatchWarningAddToRefListItemFormat;
        private string _addToNuspecFormat = AnalysisResources.ReferencesInNuspecAndRefFilesDontMatchWarningAddToNuspecListItemFormat;
        private string _addToNuspecNoTfmFormat = AnalysisResources.ReferencesInNuspecAndRefFilesDontMatchWarningAddToNuspecNoTfmListItemFormat;
        public string MessageFormat => AnalysisResources.ReferencesInNuspecAndRefFilesDontMatchWarning;

        public IEnumerable<PackagingLogMessage> Validate(PackageArchiveReader builder)
        {
            var refFiles = builder.GetFiles().Where(t => t.StartsWith(PackagingConstants.Folders.Ref, StringComparison.OrdinalIgnoreCase));
            var nuspecReferences = GetReferencesFromNuspec(builder.GetNuspec());
            var missingItems = Compare(nuspecReferences, refFiles);
            return GenerateWarnings(missingItems);
        }

        internal IDictionary<string, IEnumerable<string>> GetReferencesFromNuspec(Stream nuspecStream)
        {
            var nuspecReferences = new Dictionary<string, IEnumerable<string>>();
            var packageNuspec = new NuspecReader(nuspecStream);
            var nuspec = packageNuspec.Xml;
            if (nuspec != null)
            {
                XNamespace name = nuspec.Root!.Name.Namespace;
                var targetFrameworks = nuspec.Descendants(XName.Get("{" + name.NamespaceName + "}references")).Elements().Attributes("targetFramework");
                nuspecReferences = targetFrameworks.ToDictionary(k => NuGetFramework.Parse(k.Value).GetShortFolderName(),
                                                                k => k.Parent!.Elements().Attributes("file").Select(f => f.Value));
                var filesWithoutTFM = nuspec.Descendants(XName.Get("{" + name.NamespaceName + "}references"))
                    .Elements().Attributes("file").Select(f => f.Value);
                nuspecReferences.Add("any", filesWithoutTFM);
            }
            return nuspecReferences;
        }

        internal IEnumerable<MissingReference> Compare(IDictionary<string, IEnumerable<string>> nuspecReferences, IEnumerable<string> refFiles)
        {
            var missingReferences = new List<MissingReference>();
            if (nuspecReferences.Any())
            {
                if (refFiles.Any())
                {
                    var filesByTFM = refFiles.Where(t => t.Count(m => m == '/') > 1)
                        .GroupBy(t => NuGetFramework.ParseFolder(t.Split('/')[1]).GetShortFolderName(), t => Path.GetFileName(t));
                    var keys = GetAllKeys(filesByTFM);
                    var missingSubfolderInFiles = nuspecReferences.Keys.Where(t => !keys.Contains(t) &&
                    !NuGetFramework.ParseFolder(t).GetShortFolderName().Equals("unsupported", StringComparison.Ordinal) &&
                    !NuGetFramework.ParseFolder(t).GetShortFolderName().Equals("any", StringComparison.Ordinal));
                    if (missingSubfolderInFiles.Any())
                    {
                        var subfolder = nuspecReferences.Where(t => missingSubfolderInFiles.Contains(t.Key));
                        foreach (var item in subfolder)
                        {
                            missingReferences.Add(new MissingReference("ref", item.Key, item.Value.ToArray()));
                        }
                    }

                    foreach (var files in filesByTFM)
                    {
                        if (files.Key == "unsupported")
                        {
                            continue;
                        }

                        string[] missingNuspecReferences;
                        string[] missingFiles;
                        IEnumerable<string>? anyReferences = null;
                        if (nuspecReferences.TryGetValue(files.Key, out var currentReferences) ||
                            nuspecReferences.TryGetValue("any", out anyReferences))
                        {
                            if (anyReferences != null && currentReferences == null)
                            {
                                missingNuspecReferences = files.Where(m => !anyReferences.Contains(m)).ToArray();
                                missingFiles = anyReferences.Where(t => !files.Contains(t)).ToArray();
                            }
                            else
                            {
                                missingNuspecReferences = files.Where(m => !currentReferences!.Contains(m)).ToArray();
                                missingFiles = currentReferences!.Where(t => !files.Contains(t)).ToArray();
                            }
                        }
                        else
                        {
                            missingNuspecReferences = files.ToArray();
                            missingFiles = Array.Empty<string>();
                        }

                        if (missingFiles.Length != 0)
                        {
                            missingReferences.Add(new MissingReference("ref", files.Key, missingFiles));
                        }

                        if (missingNuspecReferences.Length != 0)
                        {
                            missingReferences.Add(new MissingReference("nuspec", files.Key, missingNuspecReferences));
                        }
                    }
                }
                else
                {
                    foreach (var item in nuspecReferences)
                    {
                        var refs = item.Value.ToArray();
                        if (refs.Length != 0)
                        {
                            missingReferences.Add(new MissingReference("ref", item.Key, refs));
                        }
                    }
                }
            }

            return missingReferences;
        }

        internal IEnumerable<PackagingLogMessage> GenerateWarnings(IEnumerable<MissingReference> missingReferences)
        {
            var issues = new List<PackagingLogMessage>();
            if (missingReferences.Any())
            {
                var message = new StringBuilder();
                message.AppendLine(MessageFormat);
                var referencesMissing = missingReferences.Where(t => t.MissingFrom == "nuspec");
                var refFilesMissing = missingReferences.Where(t => t.MissingFrom == "ref");
                foreach (var file in refFilesMissing)
                {
                    foreach (var item in file.MissingItems)
                    {
                        message.AppendLine(string.Format(CultureInfo.CurrentCulture, _addToRefFormat, item, file.Tfm));
                    }
                }

                foreach (var reference in referencesMissing)
                {
                    foreach (var item in reference.MissingItems)
                    {
                        if (reference.Tfm.Equals("any", StringComparison.Ordinal))
                        {
                            message.AppendLine(string.Format(CultureInfo.CurrentCulture, _addToNuspecNoTfmFormat, item));
                        }
                        else
                        {
                            message.AppendLine(string.Format(CultureInfo.CurrentCulture, _addToNuspecFormat, item, reference.Tfm));
                        }
                    }
                }
                issues.Add(PackagingLogMessage.CreateWarning(message.ToString(), NuGetLogCode.NU5131));
            }
            return issues;
        }

        internal class MissingReference
        {
            public string MissingFrom { get; }

            public string Tfm { get; }

            public string[] MissingItems { get; }

            public MissingReference(string missingFrom, string tfm, string[] missingItems)
            {
                MissingFrom = missingFrom;
                Tfm = tfm;
                MissingItems = missingItems;
            }
        }

        internal List<string> GetAllKeys(IEnumerable<IGrouping<string, string>> filesByTFM)
        {
            var keys = new List<string>();
            foreach (var item in filesByTFM)
            {
                keys.Add(item.Key);
            }
            return keys;
        }
    }
}