using System; using System.Linq; using System.Threading.Tasks; using PortingAssistant.Client.Model; using System.Collections.Generic; using NuGet.Versioning; namespace PortingAssistant.Client.Analysis.Utils { public static class ApiCompatiblity { private static readonly ApiRecommendation DEFAULT_RECOMMENDATION = new ApiRecommendation { RecommendedActionType = RecommendedActionType.NoRecommendation }; public static CompatibilityResult GetCompatibilityResult(PackageDetailsWithApiIndices package, CodeEntityDetails codeEntityDetails, string target = "net6.0", bool checkLesserPackage = false) { //If invocation, we will try to find it in a later package if (codeEntityDetails.CodeEntityType == CodeEntityType.Method) { if (string.IsNullOrEmpty(codeEntityDetails.Namespace)) { // codelyzer was not able to parse the symbol from the semantic model, so we can't accurately assess the compatibility. return new CompatibilityResult { Compatibility = Compatibility.UNKNOWN, CompatibleVersions = new List<string>() }; } return GetCompatibilityResult(package, codeEntityDetails.OriginalDefinition, codeEntityDetails.Package.Version, target, checkLesserPackage); } //If another node type, we will not try to find it. Compatibility will be based on the package compatibility else { var compatibilityResult = new CompatibilityResult { Compatibility = Compatibility.UNKNOWN, CompatibleVersions = new List<string>() }; if (package == null || !NuGetVersion.TryParse(codeEntityDetails.Package.Version, out var targetversion)) { return compatibilityResult; } if (package.PackageDetails.IsDeprecated) { compatibilityResult.Compatibility = Compatibility.DEPRECATED; return compatibilityResult; } //For other code entities, we just need to check if the package has a compatible target: if (package.PackageDetails.Targets.ContainsKey(target)) { compatibilityResult.Compatibility = Compatibility.COMPATIBLE; return compatibilityResult; } else { compatibilityResult.Compatibility = Compatibility.INCOMPATIBLE; } return compatibilityResult; } } public static CompatibilityResult GetCompatibilityResult( PackageDetailsWithApiIndices package, string apiMethodSignature, string packageVersion, string target = "net6.0", bool checkLesserPackage = false) { var compatibilityResult = new CompatibilityResult { Compatibility = Compatibility.UNKNOWN, CompatibleVersions = new List<string>() }; // If necessary data to determine compatibility is missing, return unknown compatibility if (package == null || apiMethodSignature == null || !NuGetVersion.TryParse(packageVersion, out var validPackageVersion)) { return compatibilityResult; } if (package.PackageDetails.IsDeprecated) { compatibilityResult.Compatibility = Compatibility.DEPRECATED; return compatibilityResult; } var apiDetails = GetApiDetails(package, apiMethodSignature); var compatiblePackageVersionsForTarget = GetCompatiblePackageVersionsForTarget(apiDetails, package, target, checkLesserPackage); // If package version is greater than the greatest compatible version, it is likely this latest version // has not been assessed and added to the compatibility datastore. If it has a lower version of the same // major that is compatible, then it will be marked as Compatible. It will be marked as Incompatible otherwise var maxCompatibleVersion = NugetVersionHelper.GetMaxVersion(compatiblePackageVersionsForTarget); if (maxCompatibleVersion != null && !maxCompatibleVersion.IsZeroVersion() && validPackageVersion.IsGreaterThan(maxCompatibleVersion)) { compatibilityResult.Compatibility = validPackageVersion.HasSameMajorAs(maxCompatibleVersion) ? Compatibility.COMPATIBLE : Compatibility.INCOMPATIBLE; } // In all other cases, just check to see if the list of compatible versions for the target framework // contains the current package version else { compatibilityResult.Compatibility = validPackageVersion.HasLowerOrEqualCompatibleVersion(compatiblePackageVersionsForTarget) ? Compatibility.COMPATIBLE : Compatibility.INCOMPATIBLE; } // CompatibleVersions are recommended as potential upgrades from current version compatibilityResult.CompatibleVersions = validPackageVersion.FindGreaterCompatibleVersions(compatiblePackageVersionsForTarget).ToList(); return compatibilityResult; } private static IEnumerable<string> GetCompatiblePackageVersionsForTarget( ApiDetails apiDetails, PackageDetailsWithApiIndices package, string target, bool checkLesserPackage) { // If ApiDetails found, use them to get compatible versions if (apiDetails == null) { if (!checkLesserPackage || package.PackageDetails.Targets == null || !package.PackageDetails.Targets.TryGetValue(target, out var compatiblePackageVersionsForTarget)) { return new List<string>(); } return compatiblePackageVersionsForTarget.ToList(); } // If ApiDetails not found, fallback to using PackageDetails to get compatible versions else if (apiDetails.Targets.TryGetValue(target, out var compatiblePackageVersionsForTarget)) { return compatiblePackageVersionsForTarget.ToList(); } return new List<string>(); } public static ApiRecommendation UpgradeStrategy( CompatibilityResult compatibilityResult, string apiMethodSignature, Task<RecommendationDetails> recommendationDetails, string target = "net6.0") { try { if (compatibilityResult?.CompatibleVersions != null) { var validVersions = compatibilityResult.GetCompatibleVersionsWithoutPreReleases(); if (validVersions.Count != 0) { return new ApiRecommendation { RecommendedActionType = RecommendedActionType.UpgradePackage, Description = validVersions.FirstOrDefault() }; } } return FetchApiRecommendation(apiMethodSignature, recommendationDetails, target); } catch { return DEFAULT_RECOMMENDATION; } } private static ApiRecommendation FetchApiRecommendation( string apiMethodSignature, Task<RecommendationDetails> recommendationDetails, string target = "net6.0") { if (apiMethodSignature != null && recommendationDetails != null) { recommendationDetails.Wait(); if (recommendationDetails.IsCompletedSuccessfully) { var recommendationActions = recommendationDetails.Result.Recommendations; var apiRecommendation = recommendationActions .Where(recommendation => recommendation != null && recommendation.Value == apiMethodSignature) .SelectMany(recommendation => recommendation.RecommendedActions) .Where(recomendation => recomendation.TargetFrameworks.Contains(target.ToLower())) .Select(recommend => recommend.Description); if (apiRecommendation != null && apiRecommendation.Count() != 0) { return new ApiRecommendation { RecommendedActionType = RecommendedActionType.ReplaceApi, Description = string.Join(",", apiRecommendation.Where(recommend => !string.IsNullOrEmpty(recommend))) }; } } } return DEFAULT_RECOMMENDATION; } private static ApiDetails GetApiDetails(PackageDetailsWithApiIndices packageDetailsWithApiIndices, string apiMethodSignature) { if (packageDetailsWithApiIndices == null || packageDetailsWithApiIndices.PackageDetails == null || packageDetailsWithApiIndices.IndexDict == null || packageDetailsWithApiIndices.PackageDetails.Api == null || apiMethodSignature == null) { return null; } var index = packageDetailsWithApiIndices.IndexDict.GetValueOrDefault(apiMethodSignature, -1); if (index >= 0 && index < packageDetailsWithApiIndices.PackageDetails.Api.Length) { return packageDetailsWithApiIndices.PackageDetails.Api[index]; } return null; } public static Dictionary<PackageVersionPair, PackageDetailsWithApiIndices> PreProcessPackageDetails(Dictionary<PackageVersionPair, Task<PackageDetails>> packageResults) { return packageResults.Select(entity => { try { entity.Value.Wait(); if (entity.Value.IsCompletedSuccessfully) { var indexDict = SignatureToIndexPreProcess(entity.Value.Result); return new Tuple<PackageVersionPair, PackageDetailsWithApiIndices>(entity.Key, new PackageDetailsWithApiIndices { PackageDetails = entity.Value.Result, IndexDict = indexDict }); } return null; } catch { return null; } }).Where(p => p != null).ToDictionary(t => t.Item1, t => t.Item2); } private static Dictionary<string, int> SignatureToIndexPreProcess(PackageDetails packageDetails) { var indexDict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); if (packageDetails == null || packageDetails.Api == null) { return indexDict; } for (int i = 0; i < packageDetails.Api.Length; i++) { var api = packageDetails.Api[i]; var signature = api.MethodSignature; if (!string.IsNullOrEmpty(signature) && !indexDict.ContainsKey(signature)) { indexDict.Add(signature, i); } var extensionSignature = GetExtensionSignature(api); if (!string.IsNullOrEmpty(extensionSignature) && !indexDict.ContainsKey(extensionSignature)) { indexDict.Add(extensionSignature, i); } } return indexDict; } private static string GetExtensionSignature(ApiDetails api) { try { if (api == null || api.MethodParameters == null || api.MethodParameters.Length == 0) { return null; } var possibleExtension = api.MethodParameters[0]; var methodSignatureIndex = api.MethodSignature.IndexOf("(") >= 0 ? api.MethodSignature.IndexOf("(") : api.MethodSignature.Length; var sliceMethodSignature = api.MethodSignature.Substring(0, methodSignatureIndex); var methondNameIndex = sliceMethodSignature.LastIndexOf(api.MethodName); var methodName = sliceMethodSignature.Substring(methondNameIndex >= 0 ? methondNameIndex : sliceMethodSignature.Length); var methodSignature = $"{possibleExtension}.{methodName}({String.Join(", ", api.MethodParameters.Skip(1))})"; return methodSignature; } catch { return null; } } } }