using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security; using System.Text; using Packages.Rider.Editor.Util; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; namespace Packages.Rider.Editor.ProjectGeneration { internal class ProjectGeneration : IGenerator { private enum ScriptingLanguage { None, CSharp } /// /// Map source extensions to ScriptingLanguages /// private static readonly Dictionary k_BuiltinSupportedExtensions = new Dictionary { { ".cs", ScriptingLanguage.CSharp }, { ".uxml", ScriptingLanguage.None }, { ".uss", ScriptingLanguage.None }, { ".shader", ScriptingLanguage.None }, { ".compute", ScriptingLanguage.None }, { ".cginc", ScriptingLanguage.None }, { ".hlsl", ScriptingLanguage.None }, { ".glslinc", ScriptingLanguage.None }, { ".template", ScriptingLanguage.None }, { ".raytrace", ScriptingLanguage.None }, { ".json", ScriptingLanguage.None}, { ".rsp", ScriptingLanguage.None}, { ".asmdef", ScriptingLanguage.None}, { ".asmref", ScriptingLanguage.None}, { ".xaml", ScriptingLanguage.None}, { ".tt", ScriptingLanguage.None}, { ".t4", ScriptingLanguage.None}, { ".ttinclude", ScriptingLanguage.None} }; private string[] m_ProjectSupportedExtensions = Array.Empty(); // Note that ProjectDirectory can be assumed to be the result of Path.GetFullPath public string ProjectDirectory { get; } public string ProjectDirectoryWithSlash { get; } private readonly string m_ProjectName; private readonly IAssemblyNameProvider m_AssemblyNameProvider; private readonly IFileIO m_FileIOProvider; private readonly IGUIDGenerator m_GUIDGenerator; private readonly Dictionary m_ProjectGuids = new Dictionary(); // If we have multiple projects, the same assembly references are reused for each. Caching the normalised paths and // names is actually cheaper than recalculating each time, in terms of both time and memory allocations private readonly Dictionary m_NormalisedPaths = new Dictionary(); private readonly Dictionary m_AssemblyNames = new Dictionary(); internal static bool isRiderProjectGeneration; // workaround to https://github.cds.internal.unity3d.com/unity/com.unity.ide.rider/issues/28 IAssemblyNameProvider IGenerator.AssemblyNameProvider => m_AssemblyNameProvider; public ProjectGeneration() : this(Directory.GetParent(Application.dataPath).FullName) { } public ProjectGeneration(string projectDirectory) : this(projectDirectory, new AssemblyNameProvider(), new FileIOProvider(), new GUIDProvider()) { } public ProjectGeneration(string projectDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator) { ProjectDirectory = Path.GetFullPath(projectDirectory.NormalizePath()); ProjectDirectoryWithSlash = ProjectDirectory + Path.DirectorySeparatorChar; m_ProjectName = Path.GetFileName(ProjectDirectory); m_AssemblyNameProvider = assemblyNameProvider; m_FileIOProvider = fileIoProvider; m_GUIDGenerator = guidGenerator; } /// /// Syncs the scripting solution if any affected files are relevant. /// /// /// Whether the solution was synced. /// /// /// A set of files whose status has changed /// /// /// A set of files that got reimported /// /// /// Check if project files were changed externally /// public bool SyncIfNeeded(IEnumerable affectedFiles, IEnumerable reimportedFiles, bool checkProjectFiles = false) { SetupSupportedExtensions(); PackageManagerTracker.SyncIfNeeded(checkProjectFiles); if (HasFilesBeenModified(affectedFiles, reimportedFiles) || RiderScriptEditorData.instance.hasChanges || RiderScriptEditorData.instance.HasChangesInCompilationDefines() || (checkProjectFiles && LastWriteTracker.HasLastWriteTimeChanged())) { Sync(); return true; } return false; } private bool HasFilesBeenModified(IEnumerable affectedFiles, IEnumerable reimportedFiles) { return affectedFiles.Any(ShouldFileBePartOfSolution) || reimportedFiles.Any(ShouldSyncOnReimportedAsset); } private static bool ShouldSyncOnReimportedAsset(string asset) { var extension = Path.GetExtension(asset); return extension == ".asmdef" || extension == ".asmref" || Path.GetFileName(asset) == "csc.rsp"; } public void Sync() { SetupSupportedExtensions(); var types = GetAssetPostprocessorTypes(); isRiderProjectGeneration = true; var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(types); isRiderProjectGeneration = false; if (!externalCodeAlreadyGeneratedProjects) { GenerateAndWriteSolutionAndProjects(types); } OnGeneratedCSProjectFiles(types); m_AssemblyNameProvider.ResetCaches(); m_AssemblyNames.Clear(); m_NormalisedPaths.Clear(); m_ProjectGuids.Clear(); _buffer = null; RiderScriptEditorData.instance.hasChanges = false; RiderScriptEditorData.instance.InvalidateSavedCompilationDefines(); } public bool HasSolutionBeenGenerated() { return m_FileIOProvider.Exists(SolutionFile()); } private void SetupSupportedExtensions() { var extensions = m_AssemblyNameProvider.ProjectSupportedExtensions; m_ProjectSupportedExtensions = new string[extensions.Length]; for (var i = 0; i < extensions.Length; i++) { m_ProjectSupportedExtensions[i] = "." + extensions[i]; } } private bool ShouldFileBePartOfSolution(string file) { // Exclude files coming from packages except if they are internalized. if (m_AssemblyNameProvider.IsInternalizedPackagePath(file)) { return false; } return HasValidExtension(file); } public bool HasValidExtension(string file) { // Dll's are not scripts but still need to be included.. if (file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) return true; var extension = Path.GetExtension(file); return IsSupportedExtension(extension); } private bool IsSupportedExtension(string extension) { return k_BuiltinSupportedExtensions.ContainsKey(extension) || m_ProjectSupportedExtensions.Contains(extension); } private class AssemblyUsage { private readonly HashSet m_ProjectAssemblies = new HashSet(); private readonly HashSet m_PrecompiledAssemblies = new HashSet(); public void AddProjectAssembly(Assembly assembly) { m_ProjectAssemblies.Add(assembly.name); } public void AddPrecompiledAssembly(Assembly assembly) { m_PrecompiledAssemblies.Add(assembly.name); } public bool IsProjectAssembly(Assembly assembly) => m_ProjectAssemblies.Contains(assembly.name); public bool IsPrecompiledAssembly(Assembly assembly) => m_PrecompiledAssemblies.Contains(assembly.name); } private void GenerateAndWriteSolutionAndProjects(Type[] types) { // Only synchronize islands that have associated source files and ones that we actually want in the project. // This also filters out DLLs coming from .asmdef files in packages. // Get all of the assemblies that Unity will compile from source. This includes Assembly-CSharp, all user assembly // definitions, and all packages. Not all of the returned assemblies will require project files - by default, // registry, git and local tarball packages are pre-compiled by Unity and will not require a project. This can be // changed by the user in the External Tools settings page. // Each assembly instance contains source files, output path, defines, compiler options and references. There // will be `compiledAssemblyReferences`, which are DLLs, such as UnityEngine.dll, and assembly references, which // are references to other assemblies that Unity will compile from source. Again, these assemblies might be // projects, or pre-compiled by Unity, depending on the options selected by the user. var allAssemblies = m_AssemblyNameProvider.GetAllAssemblies(); var assemblyUsage = new AssemblyUsage(); foreach (var assembly in allAssemblies) { if (assembly.sourceFiles.Any(ShouldFileBePartOfSolution)) assemblyUsage.AddProjectAssembly(assembly); else assemblyUsage.AddPrecompiledAssembly(assembly); } // Get additional assets (other than source files) that we want to add to the projects, e.g. shaders, asmdef, etc. var additionalAssetsByAssembly = GetAdditionalAssets(); var projectParts = new List(); var assemblyNamesWithSource = new HashSet(); foreach (var assembly in allAssemblies) { if (!assemblyUsage.IsProjectAssembly(assembly)) continue; // TODO: Will this check ever be true? Player assemblies don't have the same name as editor assemblies, right? if (assemblyNamesWithSource.Contains(assembly.name)) projectParts.Add(new ProjectPart(assembly.name, assembly, new List())); // do not add asset project parts to both editor and player projects else { additionalAssetsByAssembly.TryGetValue(assembly.name, out var additionalAssetsForProject); projectParts.Add(new ProjectPart(assembly.name, assembly, additionalAssetsForProject)); assemblyNamesWithSource.Add(assembly.name); } } // If there are any assets that should be in a separate assembly, but that assembly folder doesn't contain any // source files, we'll have orphaned assets. Create a project for these assemblies, with references based on the // Rider package assembly // TODO: Would this produce the same results if we removed the check for ShouldFileBePartOfSolution above? // I suspect the only difference would be output path and references, and potentially simplify things var executingAssemblyName = typeof(ProjectGeneration).Assembly.GetName().Name; var riderAssembly = m_AssemblyNameProvider.GetNamedAssembly(executingAssemblyName); string[] coreReferences = null; foreach (var pair in additionalAssetsByAssembly) { var assembly = pair.Key; var additionalAssets = pair.Value; if (!assemblyNamesWithSource.Contains(assembly)) { if (coreReferences == null) { coreReferences = riderAssembly?.compiledAssemblyReferences.Where(a => a.EndsWith("UnityEditor.dll", StringComparison.Ordinal) || a.EndsWith("UnityEngine.dll", StringComparison.Ordinal) || a.EndsWith("UnityEngine.CoreModule.dll", StringComparison.Ordinal)).ToArray(); } projectParts.Add(AddProjectPart(assembly, riderAssembly, coreReferences, additionalAssets)); } } var stringBuilder = new StringBuilder(); SyncSolution(stringBuilder, projectParts, types); stringBuilder.Clear(); foreach (var projectPart in projectParts) { SyncProject(stringBuilder, projectPart, assemblyUsage, types); stringBuilder.Clear(); } } private static ProjectPart AddProjectPart(string assemblyName, Assembly riderAssembly, string[] coreReferences, List additionalAssets) { Assembly assembly = null; if (riderAssembly != null) { // We want to add those references, so that Rider would detect Unity path and version and provide rich features for shader files // Note that output path will be Library/ScriptAssemblies assembly = new Assembly(assemblyName, riderAssembly.outputPath, Array.Empty(), new []{"UNITY_EDITOR"}, Array.Empty(), coreReferences, riderAssembly.flags); } return new ProjectPart(assemblyName, assembly, additionalAssets); } private Dictionary> GetAdditionalAssets() { var assemblyDllNames = new FilePathTrie(); var interestingAssets = new List(); foreach (var assetPath in m_AssemblyNameProvider.GetAllAssetPaths()) { if (m_AssemblyNameProvider.IsInternalizedPackagePath(assetPath)) continue; // Find all the .asmdef and .asmref files. Then get the assembly for a file in the same folder. Anything in that // folder or below will be in the same assembly (unless there's another nested .asmdef, obvs) if (assetPath.EndsWith(".asmdef", StringComparison.OrdinalIgnoreCase) || assetPath.EndsWith(".asmref", StringComparison.OrdinalIgnoreCase)) { // This call is very expensive when working with a very large project (e.g. called for 50,000+ assets), hence // the approach of working with assembly definition root folders. We don't need a real script file to get the // assembly DLL name var assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(assetPath + ".cs"); assemblyDllNames.Insert(Path.GetDirectoryName(assetPath), assemblyDllName); } interestingAssets.Add(assetPath); } const string fallbackAssemblyDllName = "Assembly-CSharp.dll"; var assetsByAssemblyDll = new Dictionary>(); foreach (var asset in interestingAssets) { // TODO: Can we remove folders from generated projects? // Why do we add them? We get an asset for every folder, including intermediate folders. We add folders that // contain assets that we don't add to project files, so they appear empty. Adding them to the project file does // not give us anything special - they appear as a folder in the Solution Explorer, so we can right click and // add a file, but we could also "Show All Files" and do the same. Equally, Rider defaults to the Unity Explorer // view, which shows all files and folders by default. // We gain nothing by adding folders, and for very large projects, it can be very expensive to discover what // project they should be added to, since most paths will be _above_ asmdef files, or inside Assets (which // requires the full expensive check due to Editor, Resources, etc.) // (E.g. an example large project with 45,600 assets, 5,000 are folders and only 2,500 are useful assets) if (AssetDatabase.IsValidFolder(asset)) { // var assemblyDllName = assemblyDllNames.FindClosestMatch(asset); // if (string.IsNullOrEmpty(assemblyDllName)) // { // // Can't find it in trie (Assembly-CSharp and related projects don't have .asmdef files) // assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath($"{asset}/asset.cs"); // } // if (string.IsNullOrEmpty(assemblyDllName)) // assemblyDllName = fallbackAssemblyDllName; // // if (!stringBuilders.TryGetValue(assemblyDllName, out var projectBuilder)) // { // projectBuilder = new StringBuilder(); // stringBuilders[assemblyDllName] = projectBuilder; //} // // projectBuilder.Append(" ") // .AppendLine(); } else { if (!asset.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) && IsSupportedExtension(Path.GetExtension(asset))) { var assemblyDllName = assemblyDllNames.FindClosestMatch(asset); if (string.IsNullOrEmpty(assemblyDllName)) { // Can't find it in trie (Assembly-CSharp and related projects don't have .asmdef files) assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath($"{asset}.cs"); } if (string.IsNullOrEmpty(assemblyDllName)) assemblyDllName = fallbackAssemblyDllName; if (!assetsByAssemblyDll.TryGetValue(assemblyDllName, out var assets)) { assets = new List(); assetsByAssemblyDll[assemblyDllName] = assets; } assets.Add(m_FileIOProvider.EscapedRelativePathFor(asset, ProjectDirectoryWithSlash)); } } } var assetsByAssemblyName = new Dictionary>(assetsByAssemblyDll.Count); foreach (var entry in assetsByAssemblyDll) { var assemblyName = FileSystemUtil.FileNameWithoutExtension(entry.Key); assetsByAssemblyName[assemblyName] = entry.Value; } return assetsByAssemblyName; } private void SyncProject(StringBuilder stringBuilder, ProjectPart island, AssemblyUsage assemblyUsage, Type[] types) { SyncProjectFileIfNotChanged( ProjectFile(island), ProjectText(stringBuilder, island, assemblyUsage), types); } private void SyncProjectFileIfNotChanged(string path, string newContents, Type[] types) { if (Path.GetExtension(path) == ".csproj") { newContents = OnGeneratedCSProject(path, newContents, types); } SyncFileIfNotChanged(path, newContents); } private void SyncSolutionFileIfNotChanged(string path, string newContents, Type[] types) { newContents = OnGeneratedSlnSolution(path, newContents, types); SyncFileIfNotChanged(path, newContents); } private static void OnGeneratedCSProjectFiles(Type[] types) { foreach (var type in types) { var method = type.GetMethod("OnGeneratedCSProjectFiles", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); if (method == null) { continue; } Debug.LogWarning("OnGeneratedCSProjectFiles is not supported."); // RIDER-51958 //method.Invoke(null, args); } } public static Type[] GetAssetPostprocessorTypes() { return TypeCache.GetTypesDerivedFrom().ToArray(); // doesn't find types from EditorPlugin, which is fine } private static bool OnPreGeneratingCSProjectFiles(Type[] types) { var result = false; foreach (var type in types) { var args = new object[0]; var method = type.GetMethod("OnPreGeneratingCSProjectFiles", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); if (method == null) { continue; } var returnValue = method.Invoke(null, args); if (method.ReturnType == typeof(bool)) { result |= (bool)returnValue; } } return result; } private static string OnGeneratedCSProject(string path, string content, Type[] types) { foreach (var type in types) { var args = new[] { path, content }; var method = type.GetMethod("OnGeneratedCSProject", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); if (method == null) { continue; } var returnValue = method.Invoke(null, args); if (method.ReturnType == typeof(string)) { content = (string)returnValue; } } return content; } private static string OnGeneratedSlnSolution(string path, string content, Type[] types) { foreach (var type in types) { var args = new[] { path, content }; var method = type.GetMethod("OnGeneratedSlnSolution", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); if (method == null) { continue; } var returnValue = method.Invoke(null, args); if (method.ReturnType == typeof(string)) { content = (string)returnValue; } } return content; } private void SyncFileIfNotChanged(string path, string newContents) { if (HasChanged(path, newContents)) m_FileIOProvider.WriteAllText(path, newContents); } private static char[] _buffer = null; private bool HasChanged(string path, string newContents) { try { if (!m_FileIOProvider.Exists(path)) return true; const int bufferSize = 100 * 1024; // 100kb - big enough to read most project files in a single read if (_buffer == null) _buffer = new char[bufferSize]; using (var reader = m_FileIOProvider.GetReader(path)) { int read, offset = 0; do { read = reader.ReadBlock(_buffer, 0, _buffer.Length); for (var i = 0; i < read; i++) { if (_buffer[i] != newContents[offset + i]) return true; } offset += read; } while (read > 0); var isSame = offset == newContents.Length; return !isSame; } } catch (Exception exception) { Debug.LogException(exception); return true; } } private string ProjectText(StringBuilder projectBuilder, ProjectPart assembly, AssemblyUsage assemblyUsage) { var responseFilesData = assembly.GetResponseFileData(m_AssemblyNameProvider, ProjectDirectory); ProjectHeader(projectBuilder, assembly, responseFilesData); projectBuilder.AppendLine(" "); foreach (var file in assembly.SourceFiles) { var fullFile = m_FileIOProvider.EscapedRelativePathFor(file, ProjectDirectory); projectBuilder.Append(" "); } foreach (var additionalAsset in (IEnumerable)assembly.AdditionalAssets ?? Array.Empty()) projectBuilder.Append(" "); var binaryReferences = new HashSet(assembly.CompiledAssemblyReferences); foreach (var responseFileData in responseFilesData) binaryReferences.UnionWith(responseFileData.FullPathReferences); foreach (var assemblyReference in assembly.AssemblyReferences) { if (assemblyUsage.IsPrecompiledAssembly(assemblyReference)) binaryReferences.Add(assemblyReference.outputPath); } foreach (var reference in binaryReferences) { var escapedFullPath = GetNormalisedAssemblyPath(reference); var assemblyName = GetAssemblyNameFromPath(reference); projectBuilder .Append(" ") .Append(" ").Append(escapedFullPath).AppendLine("") .AppendLine(" "); } if (0 < assembly.AssemblyReferences.Length) { projectBuilder .AppendLine(" ") .AppendLine(" "); foreach (var reference in assembly.AssemblyReferences) { if (assemblyUsage.IsProjectAssembly(reference)) { var name = m_AssemblyNameProvider.GetProjectName(reference.name, reference.defines); projectBuilder .Append(" ") .Append(" {").Append(ProjectGuid(name)).AppendLine("}") .Append(" ").Append(name).AppendLine("") .AppendLine(" "); } } } projectBuilder .AppendLine(" ") .AppendLine(" ") .AppendLine( " ") .AppendLine(""); return projectBuilder.ToString(); } private string ProjectFile(ProjectPart projectPart) { return Path.Combine(ProjectDirectory, $"{m_AssemblyNameProvider.GetProjectName(projectPart.Name, projectPart.Defines)}.csproj"); } public string SolutionFile() { return Path.Combine(ProjectDirectory, $"{m_ProjectName}.sln"); } private void ProjectHeader(StringBuilder stringBuilder, ProjectPart assembly, List responseFilesData) { var responseFilesDataArgs = GetOtherArgumentsFromResponseFilesData(responseFilesData); stringBuilder .AppendLine("") .AppendLine( "") .AppendLine(" ") .Append(" ").Append(GetLangVersion(responseFilesDataArgs["langversion"], assembly)).AppendLine("") .AppendLine( " <_TargetFrameworkDirectories>non_empty_path_generated_by_unity.rider.package") .AppendLine( " <_FullFrameworkReferenceAssemblyPaths>non_empty_path_generated_by_unity.rider.package") .AppendLine(" true"); var rulesetPaths = GetRoslynAnalyzerRulesetPaths(assembly, responseFilesDataArgs); foreach (var path in rulesetPaths) stringBuilder.Append(" ").Append(path).AppendLine(""); stringBuilder .AppendLine(" ") .AppendLine(" ") .AppendLine(" Debug") .AppendLine(" AnyCPU") .AppendLine(" 10.0.20506") .AppendLine(" 2.0") .Append(" ").Append(assembly.RootNamespace).AppendLine("") .Append(" {").Append(ProjectGuid(m_AssemblyNameProvider.GetProjectName(assembly.Name, assembly.Defines))).AppendLine("}") .AppendLine( " {E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") .AppendLine(" Library") .AppendLine(" Properties") .Append(" ").Append(assembly.Name).AppendLine("") .AppendLine(" v4.7.1") .AppendLine(" 512") .AppendLine(" .") .AppendLine(" ") .AppendLine(" ") .AppendLine(" true") .AppendLine(" full") .AppendLine(" false") .Append(" ").Append(assembly.OutputPath).AppendLine(""); var defines = new HashSet(assembly.Defines); foreach (var responseFileData in responseFilesData) defines.UnionWith(responseFileData.Defines); stringBuilder .Append(" ").CompatibleAppendJoin(';', defines).AppendLine("") .AppendLine(" prompt"); var warningLevel = responseFilesDataArgs["warn"].Concat(responseFilesDataArgs["w"]).Distinct().FirstOrDefault(); stringBuilder .Append(" ").Append(!string.IsNullOrWhiteSpace(warningLevel) ? warningLevel : "4").AppendLine("") .Append(" ").CompatibleAppendJoin(',', GetNoWarn(responseFilesDataArgs["nowarn"].ToList())).AppendLine("") .Append(" ").Append(assembly.CompilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe)).AppendLine(""); AppendWarningAsError(stringBuilder, responseFilesDataArgs["warnaserror"], responseFilesDataArgs["warnaserror-"], responseFilesDataArgs["warnaserror+"]); // TODO: Can we have multiple documentation files in a project file? foreach (var docFile in responseFilesDataArgs["doc"]) stringBuilder.Append(" ").Append(docFile).AppendLine(""); var nullable = responseFilesDataArgs["nullable"].FirstOrDefault(); if (!string.IsNullOrEmpty(nullable)) stringBuilder.Append(" ").Append(nullable).AppendLine(""); stringBuilder .AppendLine(" ") .AppendLine(" ") .AppendLine(" true") .AppendLine(" true") .AppendLine(" false") .AppendLine(" false") .AppendLine(" false") .AppendLine(" "); var analyzers = GetRoslynAnalyzers(assembly, responseFilesDataArgs); if (analyzers.Length > 0) { stringBuilder.AppendLine(" "); foreach (var analyzer in analyzers) stringBuilder.AppendLine($" "); stringBuilder.AppendLine(" "); } var additionalFiles = GetRoslynAdditionalFiles(assembly, responseFilesDataArgs); if (additionalFiles.Length > 0) { stringBuilder.AppendLine(" "); foreach (var additionalFile in additionalFiles) stringBuilder.AppendLine($" "); stringBuilder.AppendLine(" "); } var configFile = GetGlobalAnalyzerConfigFile(assembly); if (!string.IsNullOrEmpty(configFile)) { stringBuilder .AppendLine(" ") .Append(" ") .AppendLine(" "); } } private static string GetGlobalAnalyzerConfigFile(ProjectPart assembly) { var configFile = string.Empty; #if UNITY_2021_3 // https://github.com/JetBrains/resharper-unity/issues/2401 var type = assembly.CompilerOptions.GetType(); var propertyInfo = type.GetProperty("AnalyzerConfigPath"); if (propertyInfo != null && propertyInfo.GetValue(assembly.CompilerOptions) is string value) { configFile = value; } #elif UNITY_2022_2_OR_NEWER configFile = assembly.CompilerOptions.AnalyzerConfigPath; // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Compilation.ScriptCompilerOptions.AnalyzerConfigPath.html #endif return configFile; } private static string[] GetRoslynAdditionalFiles(ProjectPart assembly, ILookup otherResponseFilesData) { var additionalFilePathsFromCompilationPipeline = Array.Empty(); #if UNITY_2021_3 // https://github.com/JetBrains/resharper-unity/issues/2401 var type = assembly.CompilerOptions.GetType(); var propertyInfo = type.GetProperty("RoslynAdditionalFilePaths"); if (propertyInfo != null && propertyInfo.GetValue(assembly.CompilerOptions) is string[] value) { additionalFilePathsFromCompilationPipeline = value; } #elif UNITY_2022_2_OR_NEWER // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Compilation.ScriptCompilerOptions.RoslynAdditionalFilePaths.html additionalFilePathsFromCompilationPipeline = assembly.CompilerOptions.RoslynAdditionalFilePaths; #endif return otherResponseFilesData["additionalfile"] .SelectMany(x=>x.Split(';')) .Concat(additionalFilePathsFromCompilationPipeline) .Distinct().ToArray(); } string[] GetRoslynAnalyzers(ProjectPart assembly, ILookup otherResponseFilesData) { #if UNITY_2020_2_OR_NEWER return otherResponseFilesData["analyzer"].Concat(otherResponseFilesData["a"]) .SelectMany(x=>x.Split(';')) #if !ROSLYN_ANALYZER_FIX .Concat(m_AssemblyNameProvider.GetRoslynAnalyzerPaths()) #else .Concat(assembly.CompilerOptions.RoslynAnalyzerDllPaths) #endif .Select(GetNormalisedAssemblyPath) .Distinct() .ToArray(); #else return otherResponseFilesData["analyzer"].Concat(otherResponseFilesData["a"]) .SelectMany(x=>x.Split(';')) .Distinct() .Select(GetNormalisedAssemblyPath) .ToArray(); #endif } private IEnumerable GetRoslynAnalyzerRulesetPaths(ProjectPart assembly, ILookup otherResponseFilesData) { var paths = new HashSet(otherResponseFilesData["ruleset"]); #if UNITY_2020_2_OR_NEWER if (!string.IsNullOrEmpty(assembly.CompilerOptions.RoslynAnalyzerRulesetPath)) paths.Add(assembly.CompilerOptions.RoslynAnalyzerRulesetPath); #endif return paths.Select(GetNormalisedAssemblyPath); } private static void AppendWarningAsError(StringBuilder stringBuilder, IEnumerable args, IEnumerable argsMinus, IEnumerable argsPlus) { var treatWarningsAsErrors = false; var warningIds = new List(); var notWarningIds = new List(argsMinus); foreach (var s in args) { if (s == "+" || s == "") treatWarningsAsErrors = true; else if (s == "-") treatWarningsAsErrors = false; else warningIds.Add(s); } warningIds.AddRange(argsPlus); stringBuilder.Append(" ").Append(treatWarningsAsErrors) .AppendLine(""); if (warningIds.Count > 0) stringBuilder.Append(" ").CompatibleAppendJoin(';', warningIds).AppendLine(""); if (notWarningIds.Count > 0) stringBuilder.Append(" ").CompatibleAppendJoin(';', notWarningIds) .AppendLine(""); } private void SyncSolution(StringBuilder stringBuilder, List islands, Type[] types) { SyncSolutionFileIfNotChanged(SolutionFile(), SolutionText(stringBuilder, islands), types); } private string SolutionText(StringBuilder stringBuilder, List islands) { stringBuilder .AppendLine() .AppendLine("Microsoft Visual Studio Solution File, Format Version 11.00") .AppendLine("# Visual Studio 2010"); foreach (var island in islands) { var projectName = m_AssemblyNameProvider.GetProjectName(island.Name, island.Defines); // GUID is for C# class libraries stringBuilder .Append("Project(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"") .Append(island.Name) .Append("\", \"") .Append(projectName) .Append(".csproj\", \"{") .Append(ProjectGuid(projectName)) .AppendLine("}\"") .AppendLine("EndProject"); } stringBuilder.AppendLine("Global") .AppendLine("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution") .AppendLine("\t\tDebug|Any CPU = Debug|Any CPU") .AppendLine("\tEndGlobalSection") .AppendLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); foreach (var island in islands) { var projectGuid = ProjectGuid(m_AssemblyNameProvider.GetProjectName(island.Name, island.Defines)); stringBuilder .Append("\t\t{").Append(projectGuid).AppendLine("}.Debug|Any CPU.ActiveCfg = Debug|Any CPU") .Append("\t\t{").Append(projectGuid).AppendLine("}.Debug|Any CPU.Build.0 = Debug|Any CPU"); } stringBuilder.AppendLine("\tEndGlobalSection") .AppendLine("\tGlobalSection(SolutionProperties) = preSolution") .AppendLine("\t\tHideSolutionNode = FALSE") .AppendLine("\tEndGlobalSection") .AppendLine("EndGlobal"); return stringBuilder.ToString(); } private static ILookup GetOtherArgumentsFromResponseFilesData(List responseFilesData) { var paths = responseFilesData.SelectMany(x => { return x.OtherArguments .Where(a => a.StartsWith("/", StringComparison.Ordinal) || a.StartsWith("-", StringComparison.Ordinal)) .Select(b => { var index = b.IndexOf(":", StringComparison.Ordinal); if (index > 0 && b.Length > index) { var key = b.Substring(1, index - 1); return new KeyValuePair(key, b.Substring(index + 1)); } const string warnaserror = "warnaserror"; if (b.Substring(1).StartsWith(warnaserror, StringComparison.Ordinal)) { return new KeyValuePair(warnaserror, b.Substring(warnaserror.Length + 1)); } const string nullable = "nullable"; if (b.Substring(1).StartsWith(nullable, StringComparison.Ordinal)) { var res = b.Substring(nullable.Length + 1); if (string.IsNullOrWhiteSpace(res) || res.Equals("+")) res = "enable"; else if (res.Equals("-")) res = "disable"; return new KeyValuePair(nullable, res); } return default; }); }) .Distinct() .ToLookup(o => o.Key, pair => pair.Value); return paths; } private string GetLangVersion(IEnumerable langVersionList, ProjectPart assembly) { var langVersion = langVersionList.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(langVersion)) return langVersion; #if UNITY_2020_2_OR_NEWER return assembly.CompilerOptions.LanguageVersion; #else return "latest"; #endif } public static IEnumerable GetNoWarn(List codes) { #if UNITY_2019_4 || UNITY_2020_1 // RIDER-77206 Unity 2020.1.3 'PlayerSettings' does not contain a definition for 'suppressCommonWarnings' var type = typeof(PlayerSettings); var propertyInfo = type.GetProperty("suppressCommonWarnings"); if (propertyInfo != null && propertyInfo.GetValue(null) is bool && (bool)propertyInfo.GetValue(null)) { codes.AddRange(new[] {"0169", "0649"}); } #elif UNITY_2020_2_OR_NEWER if (PlayerSettings.suppressCommonWarnings) codes.AddRange(new[] {"0169", "0649"}); #endif return codes.Distinct(); } private string ProjectGuid(string name) { if (!m_ProjectGuids.TryGetValue(name, out var guid)) { guid = m_GUIDGenerator.ProjectGuid(m_ProjectName + name); m_ProjectGuids.Add(name, guid); } return guid; } private string GetNormalisedAssemblyPath(string path) { if (!m_NormalisedPaths.TryGetValue(path, out var normalisedPath)) { normalisedPath = Path.IsPathRooted(path) ? path : Path.GetFullPath(path); normalisedPath = SecurityElement.Escape(normalisedPath).NormalizePath(); m_NormalisedPaths.Add(path, normalisedPath); } return normalisedPath; } private string GetAssemblyNameFromPath(string path) { if (!m_AssemblyNames.TryGetValue(path, out var name)) { name = FileSystemUtil.FileNameWithoutExtension(path); m_AssemblyNames.Add(path, name); } return name; } } internal class FilePathTrie { private static readonly char[] Separators = { '\\', '/' }; private readonly TrieNode m_Root = new TrieNode(); private class TrieNode { public Dictionary Children; public TData Data; } public void Insert(string filePath, TData data) { var parts = filePath.Split(Separators); var node = m_Root; foreach (var part in parts) { if (node.Children == null) node.Children = new Dictionary(StringComparer.OrdinalIgnoreCase); // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd if (!node.Children.ContainsKey(part)) node.Children[part] = new TrieNode(); node = node.Children[part]; } node.Data = data; } public TData FindClosestMatch(string filePath) { var parts = filePath.Split(Separators); var node = m_Root; foreach (var part in parts) { if (node.Children != null && node.Children.TryGetValue(part, out var next)) node = next; else break; } return node.Data; } } }