File: Shared\ProjectDependency.cs
Web Access
Project: src\src\dotnet-svcutil\lib\src\dotnet-svcutil-lib.csproj (dotnet-svcutil-lib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using NuGet.Versioning;
 
namespace Microsoft.Tools.ServiceModel.Svcutil
{
    internal enum ProjectDependencyType
    {
        Unknown,
        Project,
        Binary,
        Package,
        Tool
    }
 
    internal class ProjectDependency : IComparable<ProjectDependency>
    {
        private const string DefaultVersion = "*";
        public const string NetCoreAppPackageID = "Microsoft.NETCore.App";
        public const string NetStandardLibraryPackageID = "NETStandard.Library";
 
        private static readonly IReadOnlyList<string> s_binaryExtensions = new List<string> { ".exe", ".dll" }.AsReadOnly();
        private static readonly IReadOnlyList<string> s_projectExtensions = new List<string> { ".csproj", ".vbproj", ".fsproj", ".vcxproj", ".esproj" }.AsReadOnly();
        private static readonly IReadOnlyList<string> s_exeExtensions = new List<string> { ".exe" }.AsReadOnly();
 
        /// <summary>
        /// The dependency name.  This can be a package or assembly name.
        /// </summary>
        public string Name { get; private set; }
 
        /// <summary>
        /// Package Version.
        /// </summary>
        public string Version { get; private set; }
 
        /// <summary>
        /// Assembly full path.
        /// </summary>
        public string FullPath { get; private set; }
 
        /// <summary>
        /// Assembly name.
        /// </summary>
        public string AssemblyName { get; private set; }
 
        /// <summary>
        /// Dependency type: package, project or binary.
        /// </summary>
        public ProjectDependencyType DependencyType { get; private set; }
 
        /// <summary>
        /// If true, this represents a framework dependency.
        /// </summary>
        public bool IsFramework { get; private set; }
 
        /// <summary>
        /// The package identity, used for describing the dependency passed to Svcutil.
        /// </summary>
        public string ReferenceIdentity { get; private set; }
 
        #region instance contruction methods
        // this ctr is private to ensure the integrity of the data which can be done only by the provided static instance creation (FromXXX) methods.
        private ProjectDependency(string filePath = "", string packageName = "", string packageVersion = "", ProjectDependencyType dependencyType = ProjectDependencyType.Unknown)
        {
            bool fileHasKnownExtension = false;
 
            // basic param check.
            if (!string.IsNullOrWhiteSpace(filePath) && Path.GetInvalidPathChars().Intersect(filePath).Count() > 0)
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, filePath, nameof(filePath)));
            }
 
            if (!string.IsNullOrWhiteSpace(packageName) && Path.GetInvalidFileNameChars().Intersect(packageName).Count() > 0)
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, packageName, nameof(packageName)));
            }
 
            if (string.IsNullOrWhiteSpace(packageVersion))
            {
                packageVersion = DefaultVersion;
            }
            else if (!IsValidVersion(packageVersion))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, packageVersion, nameof(packageVersion)));
            }
 
            if (dependencyType == ProjectDependencyType.Unknown)
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, dependencyType, nameof(dependencyType)));
            }
 
            // dependency type check.
            if (dependencyType == ProjectDependencyType.Package || dependencyType == ProjectDependencyType.Tool)
            {
                if (string.IsNullOrWhiteSpace(packageName))
                {
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, packageName, nameof(packageName)));
                }
 
                // filePath cannot be a project.
                if (s_projectExtensions.Any((ext) => String.Compare(Path.GetExtension(AssemblyName), ext, StringComparison.OrdinalIgnoreCase) == 0))
                {
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, packageName, nameof(packageName)));
                }
 
                // even when it is a Pacakge type, it may contain a valid binary, let's check.
                fileHasKnownExtension = string.IsNullOrWhiteSpace(filePath) ? false :
                     (s_binaryExtensions.Any((ext) => String.Compare(Path.GetExtension(filePath), ext, StringComparison.OrdinalIgnoreCase) == 0));
 
                // check that package name is not confused with executable name.
                if (s_exeExtensions.Any((ext) => String.Compare(Path.GetExtension(packageName), ext, StringComparison.OrdinalIgnoreCase) == 0))
                {
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, packageName, nameof(packageName)));
                }
            }
            else // Binary or Project.
            {
                if (string.IsNullOrWhiteSpace(filePath))
                {
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, filePath, nameof(filePath)));
                }
 
                if (dependencyType == ProjectDependencyType.Binary)
                {
                    // Can be a full path to an assembly or just the assembly name (with or w/o extension).
                    fileHasKnownExtension = s_binaryExtensions.Any((ext) => String.Compare(Path.GetExtension(filePath), ext, StringComparison.OrdinalIgnoreCase) == 0);
                }
                else if (dependencyType == ProjectDependencyType.Project)
                {
                    fileHasKnownExtension = s_projectExtensions.Any((ext) => String.Compare(Path.GetExtension(filePath), ext, StringComparison.OrdinalIgnoreCase) == 0);
                    if (!fileHasKnownExtension)
                    {
                        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, filePath, nameof(filePath)));
                    }
                }
            }
 
            this.FullPath = fileHasKnownExtension ? Path.GetFullPath(filePath) : filePath;
            this.DependencyType = dependencyType;
            this.Version = packageVersion;
 
            this.AssemblyName = string.IsNullOrWhiteSpace(filePath) ? packageName :
                fileHasKnownExtension ? Path.GetFileNameWithoutExtension(filePath) : Path.GetFileName(filePath);
 
            this.Name = string.IsNullOrWhiteSpace(packageName) ? this.AssemblyName : packageName;
 
            this.ReferenceIdentity = this.DependencyType == ProjectDependencyType.Package || this.DependencyType == ProjectDependencyType.Tool || !string.IsNullOrWhiteSpace(packageName) ?
                string.Format(CultureInfo.InvariantCulture, "{0}, {{{1}, {2}}}", this.AssemblyName, this.Name, this.Version) :
                this.FullPath;
 
            this.IsFramework = this.Name == NetCoreAppPackageID || this.Name == NetStandardLibraryPackageID;
        }
 
        public static ProjectDependency Parse(string dependencySpec)
        {
            // Parse text of the form below where assemblyName can be a fileName, fileName.Extension of fileFullPath.
            // spec form:
            //   [assemblyName, ]{packageName[, version]]}
            //   assemblyName[, {packageName[, version]]}]
            // examples:
            //   Microsoft.CSharp, {NetCoreApp, 2.0.0} 
            //   Microsoft.CSharp, {NetCoreApp} 
            //   Microsoft.CSharp
            //   {NetCoreApp, 2.0.0} 
            //   NetCoreApp, 2.0.0 
            //   Microsoft.CSharp.dll
            //   C:\Fx\Microsoft.CSharp.dll, {NetCoreApp, 2.0.0} 
 
            ProjectDependency dependency = null;
 
            if (string.IsNullOrWhiteSpace(dependencySpec))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorInvalidDependencyValue, dependencySpec, nameof(dependencySpec)));
            }
 
            string processedSpec = dependencySpec.Trim('\"').Trim();
            string assemblyName = string.Empty;
            bool hasParenthesis = false;
 
            int openingIdx = processedSpec.IndexOf('{');
            if (openingIdx != -1)
            {
                int closingIdx = processedSpec.IndexOf('}');
                if (closingIdx != processedSpec.Length - 1 || processedSpec.IndexOf('{', openingIdx + 1) != -1 || processedSpec.IndexOf('}', closingIdx + 1) != -1)
                {
                    // error:parenthesis in wrong place, mismatching or extras.
                    processedSpec = null;
                }
                else
                {
                    // parse full spec: assemblyName, {packageName, version}
                    assemblyName = processedSpec.Substring(0, openingIdx).Trim();
                    processedSpec = processedSpec.Substring(openingIdx + 1, closingIdx - openingIdx - 1).Trim();
 
                    if (!string.IsNullOrWhiteSpace(assemblyName))
                    {
                        if (assemblyName[assemblyName.Length - 1] == ',')
                        {
                            assemblyName = assemblyName.Substring(0, assemblyName.Length - 1).Trim();
                        }
                        else
                        {
                            processedSpec = null;
                        }
                    }
                    hasParenthesis = true;
                }
            }
 
            if (processedSpec != null)
            {
                // parse package name or file spec: '{packageName, version}' or 'c:\path\file.ext'
                var values = processedSpec.Split(new char[] { ',' });
 
                if (values.Length == 1 || values.Length == 2)
                {
                    string name = values[0].Trim();
                    string version = values.Length > 1 ? values[1].Trim() : string.Empty;
                    ProjectDependencyType dependencyType;
 
                    if (string.IsNullOrWhiteSpace(assemblyName))
                    {
                        // name may refer to a file path or a package name, let's check.
                        dependencyType = GetDependencyType(name);
                        if (dependencyType != ProjectDependencyType.Package)
                        {
                            // we are dealing with a file dependency (binary or project)
                            if (hasParenthesis)
                            {
                                // error, file treated as package: '{c:\temp\lib.dll, }'
                                dependencyType = ProjectDependencyType.Unknown;
                            }
                            else
                            {
                                assemblyName = name;
                                name = string.Empty;
                            }
                        }
                    }
                    else
                    {
                        // we have an assembly spec but it may be a full spec 'assembly, {name, version}', let's check.
                        if (string.IsNullOrWhiteSpace(name))
                        {
                            dependencyType = GetDependencyType(assemblyName);
                            if (dependencyType == ProjectDependencyType.Package)
                            {
                                name = assemblyName;
                            }
                        }
                        else
                        {
                            dependencyType = ProjectDependencyType.Package;
                        }
                    }
 
                    if (dependencyType != ProjectDependencyType.Unknown)
                    {
                        dependency = new ProjectDependency(assemblyName, name, version, dependencyType);
                    }
                }
            }
 
            if (dependency == null)
            {
                throw new FormatException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorDepenencySpecFormat, dependencySpec));
            }
 
            return dependency;
        }
 
        public static ProjectDependency FromAssembly(string assemblyPath)
        {
            return new ProjectDependency(assemblyPath, null, null, ProjectDependencyType.Binary);
        }
 
        public static ProjectDependency FromProject(string projectPath)
        {
            return new ProjectDependency(projectPath, null, null, ProjectDependencyType.Project);
        }
 
        public static ProjectDependency FromPackage(string packageName, string version)
        {
            return ProjectDependency.FromPackage(null, packageName, version);
        }
 
        public static ProjectDependency FromPackage(string assemblyName, string packageName, string version)
        {
            return new ProjectDependency(assemblyName, packageName, version, ProjectDependencyType.Package);
        }
 
        public static ProjectDependency FromCliTool(string packageName, string version)
        {
            return new ProjectDependency(null, packageName, version, ProjectDependencyType.Tool);
        }
        #endregion
 
        #region helper methods
        private static ProjectDependencyType GetDependencyType(string path)
        {
            ProjectDependencyType dependencyType;
 
            if (s_projectExtensions.Any((ext) => String.Compare(Path.GetExtension(path), ext, StringComparison.OrdinalIgnoreCase) == 0))
            {
                // project dependency: <projectpath>,<version> : C:\Source\MyProject\project.csproj
                dependencyType = ProjectDependencyType.Project;
            }
            else if (s_binaryExtensions.Any((ext) => String.Compare(Path.GetExtension(path), ext, StringComparison.OrdinalIgnoreCase) == 0))
            {
                // assembly dependency: <assemblypath> : C:\assemblies\assemblyX.dll
                dependencyType = ProjectDependencyType.Binary;
            }
            else
            {
                // package dependency: <packagename>,<version> : EntityFramework.Core,7.0.0.0
                // Note: It might be that it is a path that does not exist; it is ok to treat it as a package.
                dependencyType = ProjectDependencyType.Package;
            }
 
            return dependencyType;
        }
 
        internal static bool IsValidVersion(string version)
        {
            if (version == "*-*")
            {
                return true;
            }
            // https://semver.org/spec/v2.0.0.html
 
            bool? isValid = null;
 
            if (!string.IsNullOrWhiteSpace(version))
            {
                version = version.Trim();
 
                int starIdx = version.IndexOf('*');
                if (starIdx != -1)
                {
                    // check if there's another star
                    if (version.IndexOf('*', starIdx + 1) != -1)
                    {
                        isValid = false;
                    }
                    else if (version.Length == 1)
                    {
                        isValid = true;
                    }
                    else
                    {
                        version = version.Substring(0, starIdx - 1);
                    }
                }
 
                if (!isValid.HasValue)
                {
                    isValid = NuGetVersion.TryParse(version, out NuGetVersion nugetVersion);
                }
            }
 
            return isValid == true;
        }
        #endregion
 
        #region overrides
        public bool Equals(ProjectDependency other)
        {
            return this.CompareTo(other) == 0;
        }
 
        public override bool Equals(object obj)
        {
            return this.CompareTo(obj as ProjectDependency) == 0;
        }
 
        public override int GetHashCode()
        {
            return this.Name.ToUpperInvariant().GetHashCode();
        }
 
        public override string ToString()
        {
            return this.ReferenceIdentity;
        }
        #endregion
 
        #region IComparable
        public int CompareTo(ProjectDependency other)
        {
            if (other == null) return 1;
            if (other.DependencyType == ProjectDependencyType.Binary)
            {
                return this.DependencyType == other.DependencyType ?
                StringComparer.CurrentCultureIgnoreCase.Compare(this.FullPath, other.FullPath) :
                this.DependencyType > other.DependencyType ? 1 : -1;
            }
 
            return this.DependencyType == other.DependencyType ?
            StringComparer.CurrentCultureIgnoreCase.Compare(this.AssemblyName, other.AssemblyName) :
            this.DependencyType > other.DependencyType ? 1 : -1;
        }
        #endregion
 
        public ProjectDependency Clone()
        {
            return new ProjectDependency(this.FullPath, this.Name, this.Version, this.DependencyType);
        }
 
        internal static List<ProjectDependency> IgnorableDependencies = new List<ProjectDependency>
        {
            // Packages that fail to restore.
            ProjectDependency.FromPackage("System.Runtime.InteropServices.PInvoke","*"),
 
            // Private assets in VS templates.
            ProjectDependency.FromPackage("Microsoft.EntityFrameworkCore.Design", "*"),
            ProjectDependency.FromPackage("Microsoft.EntityFrameworkCore.SqlServer.Design","*"),
            ProjectDependency.FromPackage("Microsoft.EntityFrameworkCore.Tools", "*"),
            ProjectDependency.FromPackage("Microsoft.VisualStudio.Web.CodeGeneration.Design","*"),
            
            // Package resolved by Microsoft.VisualStudio.Web.CodeGeneration.Design.
            ProjectDependency.FromPackage("dotnet-aspnet-codegenerator-design","*"),
        };
 
        internal static void RemoveRedundantReferences(IList<ProjectDependency> dependencies)
        {
            // Target framework and ServiceModel assemblies are redundant because the first are available at runtime and
            // the second are not used as types come from the private code.
            for (int idx = dependencies.Count - 1; idx >= 0; idx--)
            {
                var dependency = dependencies[idx];
                if (dependency.IsFramework || TargetFrameworkHelper.ServiceModelPackages.Any(s => s.Name == dependency.Name))
                {
                    dependencies.RemoveAt(idx);
                }
            }
        }
    }
}