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;
}
}
}