diff --git a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs index e05cf0dc..0e509fab 100644 --- a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs +++ b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs @@ -95,6 +95,13 @@ public void InstallRoslynAnalyzerTest([Values] InstallMode installMode) AssetDatabase.Refresh(); var path = $"Assets/Packages/{analyzer.Id}.{analyzer.Version}/analyzers/dotnet/cs/ErrorProne.NET.Core.dll"; var meta = (PluginImporter)AssetImporter.GetAtPath(path); + +#if UNITY_2022_3_OR_NEWER + // somehow unity doesn't import the .dll on newer unity version + var postprocessor = new NugetAssetPostprocessor() { assetPath = path }; + postprocessor.GetType().GetMethod("OnPreprocessAsset", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(postprocessor, null); +#endif + meta.SaveAndReimport(); AssetDatabase.Refresh(); @@ -107,8 +114,8 @@ public void InstallRoslynAnalyzerTest([Values] InstallMode installMode) // Verify analyzer dll import settings meta = AssetImporter.GetAtPath(path) as PluginImporter; Assert.IsNotNull(meta, "Get meta file"); - Assert.IsFalse(meta.GetCompatibleWithAnyPlatform(), "Not compatible any platform"); - Assert.IsFalse(meta.GetCompatibleWithEditor(), "Not compatible editor"); + Assert.IsFalse(meta.GetCompatibleWithAnyPlatform(), "Expected to have set compatible with any platform to false"); + Assert.IsFalse(meta.GetCompatibleWithEditor(), "Expected to have set compatible with editor to false"); foreach (var platform in Enum.GetValues(typeof(BuildTarget))) { Assert.IsFalse( @@ -130,6 +137,70 @@ public void InstallRoslynAnalyzerTest([Values] InstallMode installMode) } } + [Test] + [TestCase("2020.2.1f1", "")] + [TestCase("2021.2.1f1", "")] // no version selected because it only supports <= 3.8 + [TestCase("2022.2.1f1", "4.0")] + [TestCase("2022.3.12f1", "4.0")] + public void InstallRoslynAnalyzerWithMultipleVersionsTest(string unityVersion, string expectedEnabledRoslynAnalyzerVersion) + { + var jsonPackageId = new NugetPackageIdentifier("System.Text.Json", "7.0.1"); + var roslynAnalyzerVersions = new[] { "3.11", "4.0", "4.4" }; + + var unityVersionType = typeof(UnityVersion); + var currentUnityVersionProperty = unityVersionType.GetProperty(nameof(UnityVersion.Current), BindingFlags.Public | BindingFlags.Static); + Assume.That(currentUnityVersionProperty, Is.Not.Null); + Assume.That(currentUnityVersionProperty.CanRead, Is.True); + Assume.That(currentUnityVersionProperty.CanWrite, Is.True); + + var oldUnityVersion = currentUnityVersionProperty.GetValue(null); + try + { + currentUnityVersionProperty.SetValue(null, new UnityVersion(unityVersion)); + NugetPackageInstaller.InstallIdentifier(jsonPackageId); + AssetDatabase.Refresh(); + + foreach (var roslynAnalyzerVersion in roslynAnalyzerVersions) + { + var path = $"Assets/Packages/{jsonPackageId.Id}.{jsonPackageId.Version}/analyzers/dotnet/roslyn{roslynAnalyzerVersion}/cs/System.Text.Json.SourceGeneration.dll"; + Assert.That(path, Does.Exist.IgnoreDirectories); + var meta = (PluginImporter)AssetImporter.GetAtPath(path); + meta.SaveAndReimport(); + +#if UNITY_2022_3_OR_NEWER + // somehow unity doesn't import the .dll on newer unity version + var postprocessor = new NugetAssetPostprocessor() { assetPath = path }; + postprocessor.GetType().GetMethod("OnPreprocessAsset", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(postprocessor, null); +#endif + } + + AssetDatabase.Refresh(); + + foreach (var roslynAnalyzerVersion in roslynAnalyzerVersions) + { + var path = $"Assets/Packages/{jsonPackageId.Id}.{jsonPackageId.Version}/analyzers/dotnet/roslyn{roslynAnalyzerVersion}/cs/System.Text.Json.SourceGeneration.dll"; + Assert.That(path, Does.Exist.IgnoreDirectories); + var meta = (PluginImporter)AssetImporter.GetAtPath(path); + Assert.IsNotNull(meta, "Get meta file"); + Assert.That(AssetDatabase.GetLabels(meta), Does.Contain("NuGetForUnity")); + Assert.IsFalse(meta.GetCompatibleWithAnyPlatform(), "Expected to have set compatible with any platform to false"); + Assert.IsFalse(meta.GetCompatibleWithEditor(), "Expected to have set compatible with editor to false"); + if (roslynAnalyzerVersion == expectedEnabledRoslynAnalyzerVersion) + { + Assert.That(AssetDatabase.GetLabels(meta), Does.Contain("RoslynAnalyzer"), $"DLL of Roslyn analyzer version '{roslynAnalyzerVersion}' should have 'RoslynAnalyzer' label"); + } + else + { + Assert.That(AssetDatabase.GetLabels(meta), Does.Not.Contain("RoslynAnalyzer"), $"DLL of Roslyn analyzer version '{roslynAnalyzerVersion}' should not have 'RoslynAnalyzer' label"); + } + } + } + finally + { + currentUnityVersionProperty.SetValue(null, oldUnityVersion); + } + } + [Test] public void InstallProtobufTest([Values] InstallMode installMode) { @@ -615,10 +686,8 @@ public void TryGetBestTargetFrameworkForCurrentSettingsTest( bool supportsNetStandard21, bool supportsNet48) { - var unityVersionType = typeof(TargetFrameworkResolver).GetNestedType("UnityVersion", BindingFlags.NonPublic); - Assume.That(unityVersionType, Is.Not.Null); - - var currentUnityVersionProperty = unityVersionType.GetProperty("Current", BindingFlags.Public | BindingFlags.Static); + var unityVersionType = typeof(UnityVersion); + var currentUnityVersionProperty = unityVersionType.GetProperty(nameof(UnityVersion.Current), BindingFlags.Public | BindingFlags.Static); Assume.That(currentUnityVersionProperty, Is.Not.Null); Assume.That(currentUnityVersionProperty.CanRead, Is.True); Assume.That(currentUnityVersionProperty.CanWrite, Is.True); @@ -635,7 +704,7 @@ public void TryGetBestTargetFrameworkForCurrentSettingsTest( try { - currentUnityVersionProperty.SetValue(null, Activator.CreateInstance(unityVersionType, unityVersion)); + currentUnityVersionProperty.SetValue(null, new UnityVersion(unityVersion)); var expectedCompatibilityLevel = useNetStandard ? ApiCompatibilityLevel.NET_Standard_2_0 : ApiCompatibilityLevel.NET_4_6; currentBuildTargetApiCompatibilityLevelProperty.SetValue(null, new Lazy(() => expectedCompatibilityLevel)); diff --git a/src/NuGetForUnity/Editor/Models/UnityVersion.cs b/src/NuGetForUnity/Editor/Models/UnityVersion.cs new file mode 100644 index 00000000..1ec45e0b --- /dev/null +++ b/src/NuGetForUnity/Editor/Models/UnityVersion.cs @@ -0,0 +1,173 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace NugetForUnity.Models +{ + /// + /// Represents a unity version. + /// + internal readonly struct UnityVersion : IComparable + { + private readonly int build; + + private readonly int major; + + private readonly int minor; + + private readonly char release; + + private readonly int revision; + + /// + /// Initializes a new instance of the struct. + /// + /// Major version number. + /// Minor version number. + /// Revision number. + /// Release flag. If 'f', official release. If 'p' patch release. + /// Build number. + public UnityVersion(int major, int minor, int revision, char release, int build) + { + this.major = major; + this.minor = minor; + this.revision = revision; + this.release = release; + this.build = build; + } + + /// + /// Initializes a new instance of the struct. + /// + /// A string representation of Unity version. + /// Cannot parse version. + [SuppressMessage("ReSharper", "MemberCanBePrivate.Local", Justification = "Called by Unit Test.")] + public UnityVersion(string version) + { + var match = Regex.Match(version, @"(\d+)\.(\d+)\.(\d+)([fpba])(\d+)"); + if (!match.Success) + { + throw new ArgumentException("Invalid unity version"); + } + + major = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + minor = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + revision = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture); + release = match.Groups[4].Value[0]; + build = int.Parse(match.Groups[5].Value, CultureInfo.InvariantCulture); + } + + /// + /// Gets current version from Application.unityVersion. + /// + [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Local", Justification = "Property setter needed for unit test")] + public static UnityVersion Current { get; private set; } = new UnityVersion(Application.unityVersion); + + /// + /// Checks to see if the left is less than the right. + /// + /// The first value to compare. + /// The second value to compare. + /// True if left is less than the right. + public static bool operator <(in UnityVersion left, in UnityVersion right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Checks to see if the left is less than or equal to the right. + /// + /// The first value to compare. + /// The second value to compare. + /// True if left is less than or equal to the right. + public static bool operator <=(in UnityVersion left, in UnityVersion right) + { + return left.CompareTo(right) <= 0; + } + + /// + /// Checks to see if the left is greater than the right. + /// + /// The first value to compare. + /// The second value to compare. + /// True if left is greater than the right. + public static bool operator >(in UnityVersion left, in UnityVersion right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Checks to see if the left is greater than or equal to the right. + /// + /// The first value to compare. + /// The second value to compare. + /// True if left is greater than or equal to the right. + public static bool operator >=(in UnityVersion left, in UnityVersion right) + { + return left.CompareTo(right) >= 0; + } + + /// + public int CompareTo(UnityVersion other) + { + return Compare(this, other); + } + + private static int Compare(in UnityVersion a, in UnityVersion b) + { + if (a.major < b.major) + { + return -1; + } + + if (a.major > b.major) + { + return 1; + } + + if (a.minor < b.minor) + { + return -1; + } + + if (a.minor > b.minor) + { + return 1; + } + + if (a.revision < b.revision) + { + return -1; + } + + if (a.revision > b.revision) + { + return 1; + } + + if (a.release < b.release) + { + return -1; + } + + if (a.release > b.release) + { + return 1; + } + + if (a.build < b.build) + { + return -1; + } + + if (a.build > b.build) + { + return 1; + } + + return 0; + } + } +} diff --git a/src/NuGetForUnity/Editor/Models/UnityVersion.cs.meta b/src/NuGetForUnity/Editor/Models/UnityVersion.cs.meta new file mode 100644 index 00000000..49394ef0 --- /dev/null +++ b/src/NuGetForUnity/Editor/Models/UnityVersion.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 294ab99fe7354275bb1500695b816801 +timeCreated: 1707812879 \ No newline at end of file diff --git a/src/NuGetForUnity/Editor/NugetAssetPostprocessor.cs b/src/NuGetForUnity/Editor/NugetAssetPostprocessor.cs index 897c91a9..7e9d546a 100644 --- a/src/NuGetForUnity/Editor/NugetAssetPostprocessor.cs +++ b/src/NuGetForUnity/Editor/NugetAssetPostprocessor.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using NugetForUnity.Configuration; using NugetForUnity.Helper; +using NugetForUnity.Models; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; @@ -46,6 +47,16 @@ public class NugetAssetPostprocessor : AssetPostprocessor /// private const string RoslynAnalyzerLabel = "RoslynAnalyzer"; + /// + /// Name of the root folder containing dotnet analyzers. + /// + private static readonly string AnalyzersRoslynVersionsFolderName = Path.Combine(AnalyzersFolderName, "dotnet"); + + /// + /// Prefix for the path of dll's of roslyn analyzers. + /// + private static readonly string AnalyzersRoslynVersionSubFolderPrefix = Path.Combine(AnalyzersRoslynVersionsFolderName, "roslyn"); + private static readonly List NonObsoleteBuildTargets = typeof(BuildTarget).GetFields(BindingFlags.Public | BindingFlags.Static) .Where(fieldInfo => fieldInfo.GetCustomAttribute(typeof(ObsoleteAttribute)) == null) .Select(fieldInfo => (BuildTarget)fieldInfo.GetValue(null)) @@ -180,6 +191,44 @@ private static string GetNuGetRepositoryPath() return ConfigurationManager.NugetConfigFile.RepositoryPath + Path.DirectorySeparatorChar; } + [CanBeNull] + private static NugetPackageVersion GetMaxSupportedRoslynVersion() + { + var unityVersion = UnityVersion.Current; + if (unityVersion >= new UnityVersion(2022, 3, 12, 'f', 1)) + { + return new NugetPackageVersion("4.3.0"); + } + + if (unityVersion >= new UnityVersion(2022, 2, 1, 'f', 1)) + { + return new NugetPackageVersion("4.1.0"); + } + + if (unityVersion >= new UnityVersion(2021, 2, 1, 'f', 1)) + { + return new NugetPackageVersion("3.8.0"); + } + + return null; + } + + [CanBeNull] + private static NugetPackageVersion GetRoslynVersionNumberFromAnalyzerPath(string analyzerAssetPath) + { + var versionPrefixStartIndex = analyzerAssetPath.IndexOf(AnalyzersRoslynVersionSubFolderPrefix, StringComparison.Ordinal); + if (versionPrefixStartIndex < 0) + { + return null; + } + + var versionStartIndex = versionPrefixStartIndex + AnalyzersRoslynVersionSubFolderPrefix.Length; + var separatorIndex = analyzerAssetPath.IndexOf(Path.DirectorySeparatorChar, versionStartIndex); + var versionLength = separatorIndex >= 0 ? separatorIndex - versionStartIndex : analyzerAssetPath.Length - versionStartIndex; + var versionString = analyzerAssetPath.Substring(versionStartIndex, versionLength); + return string.IsNullOrEmpty(versionString) ? null : new NugetPackageVersion(versionString); + } + private static void ModifyImportSettingsOfRoslynAnalyzer([NotNull] PluginImporter plugin, bool reimport) { plugin.SetCompatibleWithAnyPlatform(false); @@ -189,7 +238,39 @@ private static void ModifyImportSettingsOfRoslynAnalyzer([NotNull] PluginImporte plugin.SetExcludeFromAnyPlatform(platform, false); } - AssetDatabase.SetLabels(plugin, new[] { RoslynAnalyzerLabel, ProcessedLabel }); + var enableRoslynAnalyzer = true; + + // The nuget package can contain analyzers for multiple Roslyn versions. + // In that case, for the same package, the most recent version must be chosen out of those available for the current Unity version. + var assetPath = Path.GetFullPath(plugin.assetPath); + var assetRoslynVersion = GetRoslynVersionNumberFromAnalyzerPath(assetPath); + if (assetRoslynVersion != null) + { + var maxSupportedRoslynVersion = GetMaxSupportedRoslynVersion(); + if (maxSupportedRoslynVersion == null) + { + // the current unity version doesn't support roslyn analyzers + enableRoslynAnalyzer = false; + } + else + { + var versionPrefixIndex = assetPath.IndexOf(AnalyzersRoslynVersionsFolderName, StringComparison.Ordinal); + var analyzerVersionsRootDirectoryPath = Path.Combine(assetPath.Substring(0, versionPrefixIndex), AnalyzersRoslynVersionsFolderName); + var analyzersFolders = Directory.EnumerateDirectories(analyzerVersionsRootDirectoryPath); + var allEnabledRoslynVersions = analyzersFolders.Select(GetRoslynVersionNumberFromAnalyzerPath) + .Where(version => version != null && version.CompareTo(maxSupportedRoslynVersion) <= 0) + .ToArray(); + + // If most recent valid analyzers exist elsewhere, don't add label `RoslynAnalyzer` + var maxMatchingVersion = allEnabledRoslynVersions.Max(); + if (!allEnabledRoslynVersions.Contains(assetRoslynVersion) || assetRoslynVersion < maxMatchingVersion) + { + enableRoslynAnalyzer = false; + } + } + } + + AssetDatabase.SetLabels(plugin, enableRoslynAnalyzer ? new[] { RoslynAnalyzerLabel, ProcessedLabel } : new[] { ProcessedLabel }); if (reimport) { diff --git a/src/NuGetForUnity/Editor/TargetFrameworkResolver.cs b/src/NuGetForUnity/Editor/TargetFrameworkResolver.cs index 6e734423..70fa8102 100644 --- a/src/NuGetForUnity/Editor/TargetFrameworkResolver.cs +++ b/src/NuGetForUnity/Editor/TargetFrameworkResolver.cs @@ -6,13 +6,10 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using JetBrains.Annotations; using NugetForUnity.Models; using UnityEditor; -using UnityEngine; #region No ReShaper @@ -289,126 +286,5 @@ public TargetFrameworkSupport( public DotnetVersionCompatibilityLevel[] SupportedDotnetVersions { get; } } - - private readonly struct UnityVersion : IComparable - { - private readonly int build; - - private readonly int major; - - private readonly int minor; - - private readonly char release; - - private readonly int revision; - - public UnityVersion(int major, int minor, int revision, char release, int build) - { - this.major = major; - this.minor = minor; - this.revision = revision; - this.release = release; - this.build = build; - } - - [SuppressMessage("ReSharper", "MemberCanBePrivate.Local", Justification = "Called by Unit Test.")] - public UnityVersion(string version) - { - var match = Regex.Match(version, @"(\d+)\.(\d+)\.(\d+)([fpba])(\d+)"); - if (!match.Success) - { - throw new ArgumentException("Invalid unity version"); - } - - major = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); - minor = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); - revision = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture); - release = match.Groups[4].Value[0]; - build = int.Parse(match.Groups[5].Value, CultureInfo.InvariantCulture); - } - - [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Local", Justification = "Property setter needed for unit test")] - public static UnityVersion Current { get; private set; } = new UnityVersion(Application.unityVersion); - - public static bool operator <(UnityVersion left, UnityVersion right) - { - return left.CompareTo(right) < 0; - } - - public static bool operator <=(UnityVersion left, UnityVersion right) - { - return left.CompareTo(right) <= 0; - } - - public static bool operator >(UnityVersion left, UnityVersion right) - { - return left.CompareTo(right) > 0; - } - - public static bool operator >=(UnityVersion left, UnityVersion right) - { - return left.CompareTo(right) >= 0; - } - - public int CompareTo(UnityVersion other) - { - return Compare(this, other); - } - - private static int Compare(UnityVersion a, UnityVersion b) - { - if (a.major < b.major) - { - return -1; - } - - if (a.major > b.major) - { - return 1; - } - - if (a.minor < b.minor) - { - return -1; - } - - if (a.minor > b.minor) - { - return 1; - } - - if (a.revision < b.revision) - { - return -1; - } - - if (a.revision > b.revision) - { - return 1; - } - - if (a.release < b.release) - { - return -1; - } - - if (a.release > b.release) - { - return 1; - } - - if (a.build < b.build) - { - return -1; - } - - if (a.build > b.build) - { - return 1; - } - - return 0; - } - } } } diff --git a/src/NuGetForUnity/Editor/Ui/NugetWindow.cs b/src/NuGetForUnity/Editor/Ui/NugetWindow.cs index d0db9011..4c99cd48 100644 --- a/src/NuGetForUnity/Editor/Ui/NugetWindow.cs +++ b/src/NuGetForUnity/Editor/Ui/NugetWindow.cs @@ -1182,9 +1182,7 @@ private void DrawPackage(INugetPackage package, GUIStyle packageStyle, GUIStyle // Show the version selection dropdown only on Updates tab OR on Online tab if the package is not installed and not already in Unity if (currentTab == NugetWindowTab.UpdatesTab || - (currentTab == NugetWindowTab.OnlineTab && - installed == null && - !isAlreadyImportedInEngine)) + (currentTab == NugetWindowTab.OnlineTab && installed == null && !isAlreadyImportedInEngine)) { if (package.Versions.Count <= 1) { diff --git a/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs b/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs index f67a082c..378668c7 100644 --- a/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs +++ b/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs @@ -39,7 +39,7 @@ internal static HashSet GetAlreadyImportedEditorOnlyLibraries() /// /// Check if a package is already imported in the Unity project e.g. is a part of Unity. /// - /// The package of witch the identifier is checked. + /// The package identifier witch is checked. /// Whether to log a message with the result of the check. /// If it is included in Unity. internal static bool IsAlreadyImportedInEngine([NotNull] string packageId, bool log = true)