File: Commands\Why\WhyCommandRunner.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.CommandLine.XPlat\NuGet.CommandLine.XPlat.csproj (NuGet.CommandLine.XPlat)
// 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 enable

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
using NuGet.ProjectModel;
using Spectre.Console;

namespace NuGet.CommandLine.XPlat.Commands.Why
{
    internal class WhyCommandRunner
    {
        private const string ProjectAssetsFile = "ProjectAssetsFile";

        private readonly MSBuildAPIUtility _msbuildUtility;

        public WhyCommandRunner(MSBuildAPIUtility msbuildUtility)
        {
            _msbuildUtility = msbuildUtility;
        }

        /// <summary>
        /// Executes the 'why' command.
        /// </summary>
        /// <param name="whyCommandArgs">CLI arguments for the 'why' command.</param>
        public Task<int> ExecuteCommand(WhyCommandArgs whyCommandArgs)
        {
            bool validArgumentsUsed = ValidatePathArgument(whyCommandArgs.Path, whyCommandArgs.Logger)
                                        && ValidatePackageArgument(whyCommandArgs.Package, whyCommandArgs.Logger);
            if (!validArgumentsUsed)
            {
                return Task.FromResult(ExitCodes.InvalidArguments);
            }

            string targetPackage = whyCommandArgs.Package;
            IEnumerable<(string assetsFilePath, string? projectPath)> assetsFiles;
            try
            {
                assetsFiles = FindAssetsFiles(whyCommandArgs.Path, whyCommandArgs.Logger);
            }
            catch (ArgumentException ex)
            {
                string message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.WhyCommand_Error_ArgumentExceptionThrown,
                    ex.Message);
                whyCommandArgs.Logger.MarkupLine($"[red]{message}[/]");

                return Task.FromResult(ExitCodes.InvalidArguments);
            }

            bool anyErrors = false;
            foreach ((string assetsFilePath, string? projectPath) in assetsFiles)
            {
                LockFile? assetsFile = GetProjectAssetsFile(assetsFilePath, projectPath, whyCommandArgs.Logger);

                if (assetsFile != null)
                {
                    ValidateFrameworksOptionsExistInAssetsFile(assetsFile, whyCommandArgs.Frameworks, whyCommandArgs.Logger);

                    Dictionary<string, List<DependencyNode>?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphsForTarget(
                        assetsFile,
                        whyCommandArgs.Package,
                        whyCommandArgs.Frameworks);

                    if (dependencyGraphPerFramework != null)
                    {
                        whyCommandArgs.Logger.WriteLine(
                            string.Format(CultureInfo.CurrentCulture,
                                Strings.WhyCommand_Message_DependencyGraphsFoundInProject,
                                assetsFile.PackageSpec.Name,
                                targetPackage));

                        DependencyGraphPrinter.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger, whyCommandArgs.DotnetVersionChecker);
                    }
                    else
                    {
                        whyCommandArgs.Logger.WriteLine(
                            string.Format(CultureInfo.CurrentCulture,
                                Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject,
                                assetsFile.PackageSpec.Name,
                                targetPackage));
                    }
                }
                else
                {
                    anyErrors = true;
                }
            }

            return Task.FromResult(anyErrors ? ExitCodes.Error : ExitCodes.Success);
        }

        private IEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFiles(string path, IAnsiConsole logger)
        {
            if (XPlatUtility.IsJsonFile(path))
            {
                yield return (path, null);
                yield break;
            }

            var projectPaths = _msbuildUtility.GetListOfProjectsFromPathArgument(path);
            foreach (string projectPath in projectPaths.NoAllocEnumerate())
            {
                Project project = _msbuildUtility.GetProject(projectPath).Project;
                try
                {
                    string usingNetSdk = project.GetPropertyValue("UsingMicrosoftNETSdk");
                    if (bool.TryParse(usingNetSdk, out bool b) && b)
                    {
                        if (!MSBuildAPIUtility.IsPackageReferenceProject(project))
                        {
                            string message = string.Format(
                                CultureInfo.CurrentCulture,
                                Strings.Error_NotPRProject,
                                project.FullPath);
                            logger.MarkupLine($"[red]{message}[/]");
                            continue;
                        }

                        string? assetsFilePath = project.GetPropertyValue(ProjectAssetsFile);
                        if (string.IsNullOrEmpty(assetsFilePath) || !File.Exists(assetsFilePath))
                        {
                            string message = string.Format(
                                CultureInfo.CurrentCulture,
                                Strings.Error_AssetsFileNotFound,
                                project.FullPath);
                            logger.MarkupLine($"[red]{message}[/]");
                            continue;
                        }

                        yield return (assetsFilePath, projectPath);
                    }
                    else
                    {
                        logger.WriteLine(
                                string.Format(
                                    CultureInfo.CurrentCulture,
                                    Strings.WhyCommand_Message_NonSDKStyleProjectsAreNotSupported,
                                    project.GetPropertyValue("MSBuildProjectName")));
                    }
                }
                finally
                {
                    ProjectCollection.GlobalProjectCollection.UnloadProject(project);
                }
            }
        }

        /// <summary>
        /// Validates that the input 'path' argument is a valid path to a directory, solution file or project file.
        /// </summary>
        private bool ValidatePathArgument(string path, IAnsiConsole logger)
        {
            if (string.IsNullOrEmpty(path))
            {
                string message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.WhyCommand_Error_ArgumentCannotBeEmpty,
                    "PROJECT|SOLUTION");
                logger.MarkupLine($"[red]{message}[/]");
                return false;
            }

            // Check that the input is a valid path
            string fullPath;
            try
            {
                fullPath = Path.GetFullPath(path);
            }
            catch (ArgumentException)
            {
                string message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.WhyCommand_Error_ArgumentExceptionThrown,
                    string.Format(CultureInfo.CurrentCulture, Strings.Error_PathIsMissingOrInvalid, path));
                logger.MarkupLine($"[red]{message}[/]");
                return false;
            }

            // Check that the path is a directory, solution file, project file, or a file-based app.
            if (Directory.Exists(fullPath)
                || (File.Exists(fullPath)
                    && (XPlatUtility.IsSolutionFile(fullPath) || XPlatUtility.IsProjectFile(fullPath) || XPlatUtility.IsJsonFile(fullPath) ||
                        _msbuildUtility.VirtualProjectBuilder?.IsValidEntryPointPath(fullPath) == true)))
            {
                return true;
            }
            else
            {
                string message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.WhyCommand_Error_ArgumentExceptionThrown,
                    string.Format(CultureInfo.CurrentCulture, Strings.Error_PathIsMissingOrInvalid, path));
                logger.MarkupLine($"[red]{message}[/]");
                return false;
            }
        }

        private static bool ValidatePackageArgument(string package, IAnsiConsole logger)
        {
            if (string.IsNullOrEmpty(package))
            {
                string message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.WhyCommand_Error_ArgumentCannotBeEmpty,
                    "PACKAGE");
                logger.MarkupLine($"[red]{message}[/]");
                return false;
            }

            return true;
        }

        /// <summary>
        /// Validates that the input frameworks options have corresponding targets in the assets file. Outputs a warning message if a framework does not exist.
        /// </summary>
        private static void ValidateFrameworksOptionsExistInAssetsFile(LockFile assetsFile, List<string> inputFrameworks, IAnsiConsole logger)
        {
            foreach (var frameworkAlias in inputFrameworks)
            {
                if (assetsFile.GetTarget(frameworkAlias, runtimeIdentifier: null) == null)
                {
                    string message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.WhyCommand_Warning_AssetsFileDoesNotContainSpecifiedTarget,
                        assetsFile.Path,
                        assetsFile.PackageSpec.Name,
                        frameworkAlias);
                    logger.MarkupLine($"[yellow]{message}[/]");
                }
            }
        }

        /// <summary>
        /// Returns the path to an assets file for the given project.
        /// </summary>
        /// <param name="assetsFilePath"></param>
        /// <param name="projectPath"></param>
        /// <param name="logger">Logger for the 'why' command</param>
        /// <returns>Assets file for the given project. Returns null if there was any issue finding or parsing the assets file.</returns>
        private static LockFile? GetProjectAssetsFile(string assetsFilePath, string? projectPath, IAnsiConsole logger)
        {
            if (!File.Exists(assetsFilePath))
            {
                if (!string.IsNullOrEmpty(projectPath))
                {
                    string message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.Error_AssetsFileNotFound,
                        projectPath);
                    logger.MarkupLine($"[red]{message}[/]");
                }
                else
                {
                    string message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.Error_PathIsMissingOrInvalid,
                        assetsFilePath);
                    logger.MarkupLine($"[red]{message}[/]");
                }

                return null;
            }

            var lockFileFormat = new LockFileFormat();
            LockFile assetsFile = lockFileFormat.Read(assetsFilePath);

            // assets file validation
            if (assetsFile.PackageSpec == null
                || assetsFile.Targets == null
                || assetsFile.Targets.Count == 0)
            {
                if (string.IsNullOrEmpty(projectPath))
                {
                    string message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.WhyCommand_Error_InvalidAssetsFile_WithoutProject,
                        assetsFilePath);
                    logger.MarkupLine($"[red]{message}[/]");
                }
                else
                {
                    string message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.WhyCommand_Error_InvalidAssetsFile_WithProject,
                        assetsFilePath,
                        projectPath);
                    logger.MarkupLine($"[red]{message}[/]");
                }

                return null;
            }

            return assetsFile;
        }
    }
}