File: RestoreCommand\Utility\SpecValidationUtility.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// 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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.ProjectModel;
using NuGet.Shared;

namespace NuGet.Commands
{
    public static class SpecValidationUtility
    {
        /// <summary>
        /// Validate a dg file. This will throw a RestoreSpecException if there are errors.
        /// </summary>
        public static void ValidateDependencySpec(DependencyGraphSpec spec)
        {
            ValidateDependencySpec(spec, projectsToSkip: new HashSet<string>(), NullLogger.Instance);
        }

        public static void ValidateDependencySpec(DependencyGraphSpec spec, HashSet<string> projectsToSkip, ILogger logger)
        {
            logger ??= NullLogger.Instance;

            if (spec == null)
            {
                throw new ArgumentNullException(nameof(spec));
            }

            if (projectsToSkip == null)
            {
                throw new ArgumentNullException(nameof(projectsToSkip));
            }

            try
            {
                // Verify projects
                foreach (var projectSpec in spec.Projects)
                {
                    if (!projectsToSkip.Contains(projectSpec.FilePath))
                    {
                        ValidateProjectSpec(projectSpec, logger);
                    }
                }

                var restoreSet = new HashSet<string>(spec.Restore, StringComparer.Ordinal);
                var projectSet = new HashSet<string>(
                    spec.Projects.Select(p => p.RestoreMetadata?.ProjectUniqueName)
                    .Where(s => !string.IsNullOrEmpty(s)),
                    StringComparer.Ordinal);

                // Verify restore does not reference a project that does not exist
                foreach (var missing in restoreSet.Except(projectSet))
                {
                    var message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.SpecValidationMissingProject,
                        missing);

                    throw RestoreSpecException.Create(message, Enumerable.Empty<string>());
                }

                // Verify restore contains at least one project
                if (restoreSet.Count < 1)
                {
                    throw RestoreSpecException.Create(Strings.SpecValidationZeroRestoreRequests, Enumerable.Empty<string>());
                }
            }
            catch (Exception ex) when (!(ex is RestoreSpecException))
            {
                // Catch and wrap any unexpected exceptions
                throw RestoreSpecException.Create(
                    ex.Message,
                    Enumerable.Empty<string>(),
                    ex);
            }
        }

        public static void ValidateProjectSpec(PackageSpec spec)
        {
            ValidateProjectSpec(spec, NullLogger.Instance);
        }

        private static void ValidateProjectSpec(PackageSpec spec, ILogger logger)
        {
            if (spec == null)
            {
                throw new ArgumentNullException(nameof(spec));
            }

            // Track the spec path
            var files = new Stack<string>();
            files.Push(spec.FilePath);

            // restore metadata must exist for all project types
            var restoreMetadata = spec.RestoreMetadata;

            if (restoreMetadata == null)
            {
                var message = string.Format(CultureInfo.CurrentCulture, Strings.MissingRequiredProperty, nameof(spec.RestoreMetadata));

                throw RestoreSpecException.Create(message, files);
            }

            var projectStyle = spec.RestoreMetadata?.ProjectStyle;

            // Verify required fields for all specs
            ValidateProjectMetadata(spec, files);

            if (projectStyle == ProjectStyle.DotnetCliTool)
            {
                // Verify tool properties
                ValidateToolSpec(spec, files);
            }
            else
            {
                // Track the project path
                files.Push(restoreMetadata.ProjectPath);

                // Verify project metadata
                ValidateProjectMSBuildMetadata(spec, files);

                // Verify based on the type.
                switch (projectStyle)
                {
                    case ProjectStyle.PackageReference:
                        ValidateProjectSpecPackageReference(spec, files, logger);
                        break;

                    default:
                        ValidateProjectSpecOther(spec, files);
                        break;
                }
            }
        }

        private static void ValidateFrameworks(PackageSpec spec, IEnumerable<string> files, ILogger logger)
        {
            if (spec.TargetFrameworks == null)
            {
                throw RestoreSpecException.Create(Strings.SpecValidationNoFrameworks, files);
            }

            bool hasInvalidFrameworks = false;
            List<NuGetFramework> frameworkNames = new List<NuGetFramework>(spec.TargetFrameworks.Count);

            foreach (var framework in spec.TargetFrameworks)
            {
                frameworkNames.Add(framework.FrameworkName);

                if (!framework.FrameworkName.IsSpecificFramework)
                {
                    hasInvalidFrameworks |= true;
                    var message = string.Format(CultureInfo.CurrentCulture, Strings.SpecValidationInvalidFramework, framework.TargetAlias);
                    logger.Log(new RestoreLogMessage(LogLevel.Error, NuGetLogCode.NU1105, message) { FilePath = spec.FilePath, ProjectPath = spec.FilePath });
                }
            }

            if (hasInvalidFrameworks)
            {
                throw RestoreSpecException.Create(string.Format(CultureInfo.CurrentCulture, Strings.Invalid_Framework), files);
            }

            // Must have at least 1 framework
            if (frameworkNames.Count < 1)
            {
                throw RestoreSpecException.Create(Strings.SpecValidationNoFrameworks, files);
            }
        }

        private static void ValidateProjectSpecPackageReference(PackageSpec spec, IEnumerable<string> files, ILogger logger)
        {
            // Verify frameworks
            ValidateFrameworks(spec, files, logger);

            // NETCore may not specify a project.json file
            if (!string.IsNullOrEmpty(spec.RestoreMetadata.ProjectJsonPath))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.PropertyNotAllowedForProjectType,
                    nameof(spec.RestoreMetadata.ProjectJsonPath),
                    ProjectStyle.PackageReference.ToString());

                throw RestoreSpecException.Create(message, files);
            }

            // Output path must be set for netcore
            if (string.IsNullOrEmpty(spec.RestoreMetadata.OutputPath))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.MissingRequiredPropertyForProjectType,
                    nameof(spec.RestoreMetadata.OutputPath),
                    ProjectStyle.PackageReference.ToString());

                throw RestoreSpecException.Create(message, files);
            }

            // Original frameworks must be set for netcore
            if (spec.RestoreMetadata.OriginalTargetFrameworks.Count < 1)
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.MissingRequiredPropertyForProjectType,
                    nameof(spec.RestoreMetadata.OriginalTargetFrameworks),
                    ProjectStyle.PackageReference.ToString());

                throw RestoreSpecException.Create(message, files);
            }

            List<string> aliases = (spec.TargetFrameworks.Count > 1 || spec.RestoreMetadata.TargetFrameworks.Count > 1) ?
                spec.TargetFrameworks.Select(e => e.TargetAlias).ToList()
                : [];

            //OriginalTargetFrameworks must match the aliases.
            if (spec.RestoreMetadata.TargetFrameworks.Count > 1)
            {
                if (!EqualityUtility.OrderedEquals(aliases, spec.RestoreMetadata.OriginalTargetFrameworks, e => e, StringComparer.OrdinalIgnoreCase, StringComparer.OrdinalIgnoreCase))
                {
                    var message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.SpecValidation_OriginalTargetFrameworksMustMatchAliases,
                        string.Join(";", spec.RestoreMetadata.OriginalTargetFrameworks),
                        string.Join(";", aliases)
                        );
                    throw RestoreSpecException.Create(message, files);
                }
            }

            if (spec.TargetFrameworks.Count > 1)
            {
                var uniqueAliases = new HashSet<string>(aliases, StringComparer.OrdinalIgnoreCase);

                if (uniqueAliases.Count != aliases.Count)
                {
                    var message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.SpecValidationDuplicateTargetAlias,
                        string.Join(", ", aliases));

                    throw RestoreSpecException.Create(message, files);
                }
            }
        }


        private static void ValidateToolSpec(PackageSpec spec, IEnumerable<string> files)
        {
            var packageDependencies = GetAllDependencies(spec).ToList();

            if (packageDependencies.Count != 1
                || packageDependencies.All(e => e.LibraryRange.TypeConstraint != LibraryDependencyTarget.Package)
                || spec.TargetFrameworks.Count != 1)
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.InvalidRestoreInput,
                    spec.Name);

                throw RestoreSpecException.Create(message, files);
            }
        }

        private static void ValidateProjectSpecOther(PackageSpec spec, IEnumerable<string> files)
        {
            // Unknown project types may not have a project.json path
            if (spec.RestoreMetadata.ProjectJsonPath != null)
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.PropertyNotAllowed,
                    nameof(spec.RestoreMetadata.ProjectJsonPath));

                throw RestoreSpecException.Create(message, files);
            }

            // Unknown project types may not carry package dependencies
            var packageDependencies = GetAllDependencies(spec)
                .Where(d => d.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package));

            if (packageDependencies.Any())
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.PropertyNotAllowed,
                    "dependencies");

                throw RestoreSpecException.Create(message, files);
            }
        }

        private static void ValidateProjectMetadata(PackageSpec spec, IEnumerable<string> files)
        {
            // spec file path must be set
            if (string.IsNullOrEmpty(spec.FilePath))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.MissingRequiredProperty,
                    nameof(spec.FilePath));

                throw RestoreSpecException.Create(message, files);
            }

            // spec name must be set
            if (string.IsNullOrEmpty(spec.Name))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.MissingRequiredProperty,
                    nameof(spec.Name));

                throw RestoreSpecException.Create(message, files);
            }

            // unique name must be set
            if (string.IsNullOrEmpty(spec.RestoreMetadata.ProjectUniqueName))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.MissingRequiredProperty,
                    nameof(spec.RestoreMetadata.ProjectUniqueName));

                throw RestoreSpecException.Create(message, files);
            }

            // project name must be set
            if (string.IsNullOrEmpty(spec.RestoreMetadata.ProjectName))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.MissingRequiredProperty,
                    nameof(spec.RestoreMetadata.ProjectName));

                throw RestoreSpecException.Create(message, files);
            }

            // spec.name and spec.RestoreMetadata.ProjectName should be the same
            if (!string.Equals(spec.Name, spec.RestoreMetadata.ProjectName, StringComparison.Ordinal))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.NonMatchingProperties,
                    nameof(spec.Name),
                    spec.Name,
                    nameof(spec.RestoreMetadata.ProjectName),
                    spec.RestoreMetadata.ProjectName);

                throw RestoreSpecException.Create(message, files);
            }
        }

        private static void ValidateProjectMSBuildMetadata(PackageSpec spec, IEnumerable<string> files)
        {
            // msbuild project path must be set
            if (string.IsNullOrEmpty(spec.RestoreMetadata.ProjectPath))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.MissingRequiredProperty,
                    nameof(spec.RestoreMetadata.ProjectPath));

                throw RestoreSpecException.Create(message, files);
            }

            // block xproj
            if (spec.RestoreMetadata.ProjectPath.EndsWith(".xproj", StringComparison.OrdinalIgnoreCase))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.Error_XPROJNotAllowed,
                    nameof(spec.RestoreMetadata.ProjectPath));

                throw RestoreSpecException.Create(message, files);
            }
        }

        private static IEnumerable<LibraryDependency> GetAllDependencies(PackageSpec spec)
        {
            return spec.TargetFrameworks.SelectMany(f => f.Dependencies);
        }
    }
}