Nav apraksta
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

ProjectGeneration.cs 42KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Security;
  6. using System.Text;
  7. using Packages.Rider.Editor.Util;
  8. using UnityEditor;
  9. using UnityEditor.Compilation;
  10. using UnityEngine;
  11. namespace Packages.Rider.Editor.ProjectGeneration
  12. {
  13. internal class ProjectGeneration : IGenerator
  14. {
  15. private enum ScriptingLanguage
  16. {
  17. None,
  18. CSharp
  19. }
  20. /// <summary>
  21. /// Map source extensions to ScriptingLanguages
  22. /// </summary>
  23. private static readonly Dictionary<string, ScriptingLanguage> k_BuiltinSupportedExtensions =
  24. new Dictionary<string, ScriptingLanguage>
  25. {
  26. { ".cs", ScriptingLanguage.CSharp },
  27. { ".uxml", ScriptingLanguage.None },
  28. { ".uss", ScriptingLanguage.None },
  29. { ".shader", ScriptingLanguage.None },
  30. { ".compute", ScriptingLanguage.None },
  31. { ".cginc", ScriptingLanguage.None },
  32. { ".hlsl", ScriptingLanguage.None },
  33. { ".glslinc", ScriptingLanguage.None },
  34. { ".template", ScriptingLanguage.None },
  35. { ".raytrace", ScriptingLanguage.None },
  36. { ".json", ScriptingLanguage.None},
  37. { ".rsp", ScriptingLanguage.None},
  38. { ".asmdef", ScriptingLanguage.None},
  39. { ".asmref", ScriptingLanguage.None},
  40. { ".xaml", ScriptingLanguage.None},
  41. { ".tt", ScriptingLanguage.None},
  42. { ".t4", ScriptingLanguage.None},
  43. { ".ttinclude", ScriptingLanguage.None}
  44. };
  45. private string[] m_ProjectSupportedExtensions = Array.Empty<string>();
  46. // Note that ProjectDirectory can be assumed to be the result of Path.GetFullPath
  47. public string ProjectDirectory { get; }
  48. public string ProjectDirectoryWithSlash { get; }
  49. private readonly string m_ProjectName;
  50. private readonly IAssemblyNameProvider m_AssemblyNameProvider;
  51. private readonly IFileIO m_FileIOProvider;
  52. private readonly IGUIDGenerator m_GUIDGenerator;
  53. private readonly Dictionary<string, string> m_ProjectGuids = new Dictionary<string, string>();
  54. // If we have multiple projects, the same assembly references are reused for each. Caching the normalised paths and
  55. // names is actually cheaper than recalculating each time, in terms of both time and memory allocations
  56. private readonly Dictionary<string, string> m_NormalisedPaths = new Dictionary<string, string>();
  57. private readonly Dictionary<string, string> m_AssemblyNames = new Dictionary<string, string>();
  58. internal static bool isRiderProjectGeneration; // workaround to https://github.cds.internal.unity3d.com/unity/com.unity.ide.rider/issues/28
  59. IAssemblyNameProvider IGenerator.AssemblyNameProvider => m_AssemblyNameProvider;
  60. public ProjectGeneration()
  61. : this(Directory.GetParent(Application.dataPath).FullName) { }
  62. public ProjectGeneration(string projectDirectory)
  63. : this(projectDirectory, new AssemblyNameProvider(), new FileIOProvider(), new GUIDProvider()) { }
  64. public ProjectGeneration(string projectDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator)
  65. {
  66. ProjectDirectory = Path.GetFullPath(projectDirectory.NormalizePath());
  67. ProjectDirectoryWithSlash = ProjectDirectory + Path.DirectorySeparatorChar;
  68. m_ProjectName = Path.GetFileName(ProjectDirectory);
  69. m_AssemblyNameProvider = assemblyNameProvider;
  70. m_FileIOProvider = fileIoProvider;
  71. m_GUIDGenerator = guidGenerator;
  72. }
  73. /// <summary>
  74. /// Syncs the scripting solution if any affected files are relevant.
  75. /// </summary>
  76. /// <returns>
  77. /// Whether the solution was synced.
  78. /// </returns>
  79. /// <param name='affectedFiles'>
  80. /// A set of files whose status has changed
  81. /// </param>
  82. /// <param name="reimportedFiles">
  83. /// A set of files that got reimported
  84. /// </param>
  85. /// <param name="checkProjectFiles">
  86. /// Check if project files were changed externally
  87. /// </param>
  88. public bool SyncIfNeeded(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles, bool checkProjectFiles = false)
  89. {
  90. SetupSupportedExtensions();
  91. PackageManagerTracker.SyncIfNeeded(checkProjectFiles);
  92. if (HasFilesBeenModified(affectedFiles, reimportedFiles) || RiderScriptEditorData.instance.hasChanges
  93. || RiderScriptEditorData.instance.HasChangesInCompilationDefines()
  94. || (checkProjectFiles && LastWriteTracker.HasLastWriteTimeChanged()))
  95. {
  96. Sync();
  97. return true;
  98. }
  99. return false;
  100. }
  101. private bool HasFilesBeenModified(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles)
  102. {
  103. return affectedFiles.Any(ShouldFileBePartOfSolution) || reimportedFiles.Any(ShouldSyncOnReimportedAsset);
  104. }
  105. private static bool ShouldSyncOnReimportedAsset(string asset)
  106. {
  107. var extension = Path.GetExtension(asset);
  108. return extension == ".asmdef" || extension == ".asmref" || Path.GetFileName(asset) == "csc.rsp";
  109. }
  110. public void Sync()
  111. {
  112. SetupSupportedExtensions();
  113. var types = GetAssetPostprocessorTypes();
  114. isRiderProjectGeneration = true;
  115. var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(types);
  116. isRiderProjectGeneration = false;
  117. if (!externalCodeAlreadyGeneratedProjects)
  118. {
  119. GenerateAndWriteSolutionAndProjects(types);
  120. }
  121. OnGeneratedCSProjectFiles(types);
  122. m_AssemblyNameProvider.ResetCaches();
  123. m_AssemblyNames.Clear();
  124. m_NormalisedPaths.Clear();
  125. m_ProjectGuids.Clear();
  126. _buffer = null;
  127. RiderScriptEditorData.instance.hasChanges = false;
  128. RiderScriptEditorData.instance.InvalidateSavedCompilationDefines();
  129. }
  130. public bool HasSolutionBeenGenerated()
  131. {
  132. return m_FileIOProvider.Exists(SolutionFile());
  133. }
  134. private void SetupSupportedExtensions()
  135. {
  136. var extensions = m_AssemblyNameProvider.ProjectSupportedExtensions;
  137. m_ProjectSupportedExtensions = new string[extensions.Length];
  138. for (var i = 0; i < extensions.Length; i++)
  139. {
  140. m_ProjectSupportedExtensions[i] = "." + extensions[i];
  141. }
  142. }
  143. private bool ShouldFileBePartOfSolution(string file)
  144. {
  145. // Exclude files coming from packages except if they are internalized.
  146. if (m_AssemblyNameProvider.IsInternalizedPackagePath(file))
  147. {
  148. return false;
  149. }
  150. return HasValidExtension(file);
  151. }
  152. public bool HasValidExtension(string file)
  153. {
  154. // Dll's are not scripts but still need to be included..
  155. if (file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
  156. return true;
  157. var extension = Path.GetExtension(file);
  158. return IsSupportedExtension(extension);
  159. }
  160. private bool IsSupportedExtension(string extension)
  161. {
  162. return k_BuiltinSupportedExtensions.ContainsKey(extension) || m_ProjectSupportedExtensions.Contains(extension);
  163. }
  164. private class AssemblyUsage
  165. {
  166. private readonly HashSet<string> m_ProjectAssemblies = new HashSet<string>();
  167. private readonly HashSet<string> m_PrecompiledAssemblies = new HashSet<string>();
  168. public void AddProjectAssembly(Assembly assembly)
  169. {
  170. m_ProjectAssemblies.Add(assembly.name);
  171. }
  172. public void AddPrecompiledAssembly(Assembly assembly)
  173. {
  174. m_PrecompiledAssemblies.Add(assembly.name);
  175. }
  176. public bool IsProjectAssembly(Assembly assembly) => m_ProjectAssemblies.Contains(assembly.name);
  177. public bool IsPrecompiledAssembly(Assembly assembly) => m_PrecompiledAssemblies.Contains(assembly.name);
  178. }
  179. private void GenerateAndWriteSolutionAndProjects(Type[] types)
  180. {
  181. // Only synchronize islands that have associated source files and ones that we actually want in the project.
  182. // This also filters out DLLs coming from .asmdef files in packages.
  183. // Get all of the assemblies that Unity will compile from source. This includes Assembly-CSharp, all user assembly
  184. // definitions, and all packages. Not all of the returned assemblies will require project files - by default,
  185. // registry, git and local tarball packages are pre-compiled by Unity and will not require a project. This can be
  186. // changed by the user in the External Tools settings page.
  187. // Each assembly instance contains source files, output path, defines, compiler options and references. There
  188. // will be `compiledAssemblyReferences`, which are DLLs, such as UnityEngine.dll, and assembly references, which
  189. // are references to other assemblies that Unity will compile from source. Again, these assemblies might be
  190. // projects, or pre-compiled by Unity, depending on the options selected by the user.
  191. var allAssemblies = m_AssemblyNameProvider.GetAllAssemblies();
  192. var assemblyUsage = new AssemblyUsage();
  193. foreach (var assembly in allAssemblies)
  194. {
  195. if (assembly.sourceFiles.Any(ShouldFileBePartOfSolution))
  196. assemblyUsage.AddProjectAssembly(assembly);
  197. else
  198. assemblyUsage.AddPrecompiledAssembly(assembly);
  199. }
  200. // Get additional assets (other than source files) that we want to add to the projects, e.g. shaders, asmdef, etc.
  201. var additionalAssetsByAssembly = GetAdditionalAssets();
  202. var projectParts = new List<ProjectPart>();
  203. var assemblyNamesWithSource = new HashSet<string>();
  204. foreach (var assembly in allAssemblies)
  205. {
  206. if (!assemblyUsage.IsProjectAssembly(assembly))
  207. continue;
  208. // TODO: Will this check ever be true? Player assemblies don't have the same name as editor assemblies, right?
  209. if (assemblyNamesWithSource.Contains(assembly.name))
  210. projectParts.Add(new ProjectPart(assembly.name, assembly, new List<string>())); // do not add asset project parts to both editor and player projects
  211. else
  212. {
  213. additionalAssetsByAssembly.TryGetValue(assembly.name, out var additionalAssetsForProject);
  214. projectParts.Add(new ProjectPart(assembly.name, assembly, additionalAssetsForProject));
  215. assemblyNamesWithSource.Add(assembly.name);
  216. }
  217. }
  218. // If there are any assets that should be in a separate assembly, but that assembly folder doesn't contain any
  219. // source files, we'll have orphaned assets. Create a project for these assemblies, with references based on the
  220. // Rider package assembly
  221. // TODO: Would this produce the same results if we removed the check for ShouldFileBePartOfSolution above?
  222. // I suspect the only difference would be output path and references, and potentially simplify things
  223. var executingAssemblyName = typeof(ProjectGeneration).Assembly.GetName().Name;
  224. var riderAssembly = m_AssemblyNameProvider.GetNamedAssembly(executingAssemblyName);
  225. string[] coreReferences = null;
  226. foreach (var pair in additionalAssetsByAssembly)
  227. {
  228. var assembly = pair.Key;
  229. var additionalAssets = pair.Value;
  230. if (!assemblyNamesWithSource.Contains(assembly))
  231. {
  232. if (coreReferences == null)
  233. {
  234. coreReferences = riderAssembly?.compiledAssemblyReferences.Where(a =>
  235. a.EndsWith("UnityEditor.dll", StringComparison.Ordinal) ||
  236. a.EndsWith("UnityEngine.dll", StringComparison.Ordinal) ||
  237. a.EndsWith("UnityEngine.CoreModule.dll", StringComparison.Ordinal)).ToArray();
  238. }
  239. projectParts.Add(AddProjectPart(assembly, riderAssembly, coreReferences, additionalAssets));
  240. }
  241. }
  242. var stringBuilder = new StringBuilder();
  243. SyncSolution(stringBuilder, projectParts, types);
  244. stringBuilder.Clear();
  245. foreach (var projectPart in projectParts)
  246. {
  247. SyncProject(stringBuilder, projectPart, assemblyUsage, types);
  248. stringBuilder.Clear();
  249. }
  250. }
  251. private static ProjectPart AddProjectPart(string assemblyName, Assembly riderAssembly, string[] coreReferences,
  252. List<string> additionalAssets)
  253. {
  254. Assembly assembly = null;
  255. if (riderAssembly != null)
  256. {
  257. // We want to add those references, so that Rider would detect Unity path and version and provide rich features for shader files
  258. // Note that output path will be Library/ScriptAssemblies
  259. assembly = new Assembly(assemblyName, riderAssembly.outputPath, Array.Empty<string>(),
  260. new []{"UNITY_EDITOR"},
  261. Array.Empty<Assembly>(),
  262. coreReferences,
  263. riderAssembly.flags);
  264. }
  265. return new ProjectPart(assemblyName, assembly, additionalAssets);
  266. }
  267. private Dictionary<string, List<string>> GetAdditionalAssets()
  268. {
  269. var assemblyDllNames = new FilePathTrie<string>();
  270. var interestingAssets = new List<string>();
  271. foreach (var assetPath in m_AssemblyNameProvider.GetAllAssetPaths())
  272. {
  273. if (m_AssemblyNameProvider.IsInternalizedPackagePath(assetPath))
  274. continue;
  275. // Find all the .asmdef and .asmref files. Then get the assembly for a file in the same folder. Anything in that
  276. // folder or below will be in the same assembly (unless there's another nested .asmdef, obvs)
  277. if (assetPath.EndsWith(".asmdef", StringComparison.OrdinalIgnoreCase)
  278. || assetPath.EndsWith(".asmref", StringComparison.OrdinalIgnoreCase))
  279. {
  280. // This call is very expensive when working with a very large project (e.g. called for 50,000+ assets), hence
  281. // the approach of working with assembly definition root folders. We don't need a real script file to get the
  282. // assembly DLL name
  283. var assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(assetPath + ".cs");
  284. assemblyDllNames.Insert(Path.GetDirectoryName(assetPath), assemblyDllName);
  285. }
  286. interestingAssets.Add(assetPath);
  287. }
  288. const string fallbackAssemblyDllName = "Assembly-CSharp.dll";
  289. var assetsByAssemblyDll = new Dictionary<string, List<string>>();
  290. foreach (var asset in interestingAssets)
  291. {
  292. // TODO: Can we remove folders from generated projects?
  293. // Why do we add them? We get an asset for every folder, including intermediate folders. We add folders that
  294. // contain assets that we don't add to project files, so they appear empty. Adding them to the project file does
  295. // not give us anything special - they appear as a folder in the Solution Explorer, so we can right click and
  296. // add a file, but we could also "Show All Files" and do the same. Equally, Rider defaults to the Unity Explorer
  297. // view, which shows all files and folders by default.
  298. // We gain nothing by adding folders, and for very large projects, it can be very expensive to discover what
  299. // project they should be added to, since most paths will be _above_ asmdef files, or inside Assets (which
  300. // requires the full expensive check due to Editor, Resources, etc.)
  301. // (E.g. an example large project with 45,600 assets, 5,000 are folders and only 2,500 are useful assets)
  302. if (AssetDatabase.IsValidFolder(asset))
  303. {
  304. // var assemblyDllName = assemblyDllNames.FindClosestMatch(asset);
  305. // if (string.IsNullOrEmpty(assemblyDllName))
  306. // {
  307. // // Can't find it in trie (Assembly-CSharp and related projects don't have .asmdef files)
  308. // assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath($"{asset}/asset.cs");
  309. // }
  310. // if (string.IsNullOrEmpty(assemblyDllName))
  311. // assemblyDllName = fallbackAssemblyDllName;
  312. //
  313. // if (!stringBuilders.TryGetValue(assemblyDllName, out var projectBuilder))
  314. // {
  315. // projectBuilder = new StringBuilder();
  316. // stringBuilders[assemblyDllName] = projectBuilder;
  317. //}
  318. //
  319. // projectBuilder.Append(" <Folder Include=\"")
  320. // .Append(m_FileIOProvider.EscapedRelativePathFor(asset, ProjectDirectoryWithSlash))
  321. // .Append("\" />")
  322. // .AppendLine();
  323. }
  324. else
  325. {
  326. if (!asset.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) && IsSupportedExtension(Path.GetExtension(asset)))
  327. {
  328. var assemblyDllName = assemblyDllNames.FindClosestMatch(asset);
  329. if (string.IsNullOrEmpty(assemblyDllName))
  330. {
  331. // Can't find it in trie (Assembly-CSharp and related projects don't have .asmdef files)
  332. assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath($"{asset}.cs");
  333. }
  334. if (string.IsNullOrEmpty(assemblyDllName))
  335. assemblyDllName = fallbackAssemblyDllName;
  336. if (!assetsByAssemblyDll.TryGetValue(assemblyDllName, out var assets))
  337. {
  338. assets = new List<string>();
  339. assetsByAssemblyDll[assemblyDllName] = assets;
  340. }
  341. assets.Add(m_FileIOProvider.EscapedRelativePathFor(asset, ProjectDirectoryWithSlash));
  342. }
  343. }
  344. }
  345. var assetsByAssemblyName = new Dictionary<string, List<string>>(assetsByAssemblyDll.Count);
  346. foreach (var entry in assetsByAssemblyDll)
  347. {
  348. var assemblyName = FileSystemUtil.FileNameWithoutExtension(entry.Key);
  349. assetsByAssemblyName[assemblyName] = entry.Value;
  350. }
  351. return assetsByAssemblyName;
  352. }
  353. private void SyncProject(StringBuilder stringBuilder, ProjectPart island, AssemblyUsage assemblyUsage, Type[] types)
  354. {
  355. SyncProjectFileIfNotChanged(
  356. ProjectFile(island),
  357. ProjectText(stringBuilder, island, assemblyUsage),
  358. types);
  359. }
  360. private void SyncProjectFileIfNotChanged(string path, string newContents, Type[] types)
  361. {
  362. if (Path.GetExtension(path) == ".csproj")
  363. {
  364. newContents = OnGeneratedCSProject(path, newContents, types);
  365. }
  366. SyncFileIfNotChanged(path, newContents);
  367. }
  368. private void SyncSolutionFileIfNotChanged(string path, string newContents, Type[] types)
  369. {
  370. newContents = OnGeneratedSlnSolution(path, newContents, types);
  371. SyncFileIfNotChanged(path, newContents);
  372. }
  373. private static void OnGeneratedCSProjectFiles(Type[] types)
  374. {
  375. foreach (var type in types)
  376. {
  377. var method = type.GetMethod("OnGeneratedCSProjectFiles",
  378. System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
  379. System.Reflection.BindingFlags.Static);
  380. if (method == null)
  381. {
  382. continue;
  383. }
  384. Debug.LogWarning("OnGeneratedCSProjectFiles is not supported.");
  385. // RIDER-51958
  386. //method.Invoke(null, args);
  387. }
  388. }
  389. public static Type[] GetAssetPostprocessorTypes()
  390. {
  391. return TypeCache.GetTypesDerivedFrom<AssetPostprocessor>().ToArray(); // doesn't find types from EditorPlugin, which is fine
  392. }
  393. private static bool OnPreGeneratingCSProjectFiles(Type[] types)
  394. {
  395. var result = false;
  396. foreach (var type in types)
  397. {
  398. var args = new object[0];
  399. var method = type.GetMethod("OnPreGeneratingCSProjectFiles",
  400. System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
  401. System.Reflection.BindingFlags.Static);
  402. if (method == null)
  403. {
  404. continue;
  405. }
  406. var returnValue = method.Invoke(null, args);
  407. if (method.ReturnType == typeof(bool))
  408. {
  409. result |= (bool)returnValue;
  410. }
  411. }
  412. return result;
  413. }
  414. private static string OnGeneratedCSProject(string path, string content, Type[] types)
  415. {
  416. foreach (var type in types)
  417. {
  418. var args = new[] { path, content };
  419. var method = type.GetMethod("OnGeneratedCSProject",
  420. System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
  421. System.Reflection.BindingFlags.Static);
  422. if (method == null)
  423. {
  424. continue;
  425. }
  426. var returnValue = method.Invoke(null, args);
  427. if (method.ReturnType == typeof(string))
  428. {
  429. content = (string)returnValue;
  430. }
  431. }
  432. return content;
  433. }
  434. private static string OnGeneratedSlnSolution(string path, string content, Type[] types)
  435. {
  436. foreach (var type in types)
  437. {
  438. var args = new[] { path, content };
  439. var method = type.GetMethod("OnGeneratedSlnSolution",
  440. System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
  441. System.Reflection.BindingFlags.Static);
  442. if (method == null)
  443. {
  444. continue;
  445. }
  446. var returnValue = method.Invoke(null, args);
  447. if (method.ReturnType == typeof(string))
  448. {
  449. content = (string)returnValue;
  450. }
  451. }
  452. return content;
  453. }
  454. private void SyncFileIfNotChanged(string path, string newContents)
  455. {
  456. if (HasChanged(path, newContents))
  457. m_FileIOProvider.WriteAllText(path, newContents);
  458. }
  459. private static char[] _buffer = null;
  460. private bool HasChanged(string path, string newContents)
  461. {
  462. try
  463. {
  464. if (!m_FileIOProvider.Exists(path))
  465. return true;
  466. const int bufferSize = 100 * 1024; // 100kb - big enough to read most project files in a single read
  467. if (_buffer == null)
  468. _buffer = new char[bufferSize];
  469. using (var reader = m_FileIOProvider.GetReader(path))
  470. {
  471. int read, offset = 0;
  472. do
  473. {
  474. read = reader.ReadBlock(_buffer, 0, _buffer.Length);
  475. for (var i = 0; i < read; i++)
  476. {
  477. if (_buffer[i] != newContents[offset + i])
  478. return true;
  479. }
  480. offset += read;
  481. } while (read > 0);
  482. var isSame = offset == newContents.Length;
  483. return !isSame;
  484. }
  485. }
  486. catch (Exception exception)
  487. {
  488. Debug.LogException(exception);
  489. return true;
  490. }
  491. }
  492. private string ProjectText(StringBuilder projectBuilder, ProjectPart assembly, AssemblyUsage assemblyUsage)
  493. {
  494. var responseFilesData = assembly.GetResponseFileData(m_AssemblyNameProvider, ProjectDirectory);
  495. ProjectHeader(projectBuilder, assembly, responseFilesData);
  496. projectBuilder.AppendLine(" <ItemGroup>");
  497. foreach (var file in assembly.SourceFiles)
  498. {
  499. var fullFile = m_FileIOProvider.EscapedRelativePathFor(file, ProjectDirectory);
  500. projectBuilder.Append(" <Compile Include=\"").Append(fullFile).AppendLine("\" />");
  501. }
  502. foreach (var additionalAsset in (IEnumerable<string>)assembly.AdditionalAssets ?? Array.Empty<string>())
  503. projectBuilder.Append(" <None Include=\"").Append(additionalAsset).AppendLine("\" />");
  504. var binaryReferences = new HashSet<string>(assembly.CompiledAssemblyReferences);
  505. foreach (var responseFileData in responseFilesData)
  506. binaryReferences.UnionWith(responseFileData.FullPathReferences);
  507. foreach (var assemblyReference in assembly.AssemblyReferences)
  508. {
  509. if (assemblyUsage.IsPrecompiledAssembly(assemblyReference))
  510. binaryReferences.Add(assemblyReference.outputPath);
  511. }
  512. foreach (var reference in binaryReferences)
  513. {
  514. var escapedFullPath = GetNormalisedAssemblyPath(reference);
  515. var assemblyName = GetAssemblyNameFromPath(reference);
  516. projectBuilder
  517. .Append(" <Reference Include=\"").Append(assemblyName).AppendLine("\">")
  518. .Append(" <HintPath>").Append(escapedFullPath).AppendLine("</HintPath>")
  519. .AppendLine(" </Reference>");
  520. }
  521. if (0 < assembly.AssemblyReferences.Length)
  522. {
  523. projectBuilder
  524. .AppendLine(" </ItemGroup>")
  525. .AppendLine(" <ItemGroup>");
  526. foreach (var reference in assembly.AssemblyReferences)
  527. {
  528. if (assemblyUsage.IsProjectAssembly(reference))
  529. {
  530. var name = m_AssemblyNameProvider.GetProjectName(reference.name, reference.defines);
  531. projectBuilder
  532. .Append(" <ProjectReference Include=\"").Append(name).AppendLine(".csproj\">")
  533. .Append(" <Project>{").Append(ProjectGuid(name)).AppendLine("}</Project>")
  534. .Append(" <Name>").Append(name).AppendLine("</Name>")
  535. .AppendLine(" </ProjectReference>");
  536. }
  537. }
  538. }
  539. projectBuilder
  540. .AppendLine(" </ItemGroup>")
  541. .AppendLine(" <Import Project=\"$(MSBuildToolsPath)\\Microsoft.CSharp.targets\" />")
  542. .AppendLine(
  543. " <!-- To modify your build process, add your task inside one of the targets below and uncomment it.")
  544. .AppendLine(" Other similar extension points exist, see Microsoft.Common.targets.")
  545. .AppendLine(" <Target Name=\"BeforeBuild\">")
  546. .AppendLine(" </Target>")
  547. .AppendLine(" <Target Name=\"AfterBuild\">")
  548. .AppendLine(" </Target>")
  549. .AppendLine(" -->")
  550. .AppendLine("</Project>");
  551. return projectBuilder.ToString();
  552. }
  553. private string ProjectFile(ProjectPart projectPart)
  554. {
  555. return Path.Combine(ProjectDirectory, $"{m_AssemblyNameProvider.GetProjectName(projectPart.Name, projectPart.Defines)}.csproj");
  556. }
  557. public string SolutionFile()
  558. {
  559. return Path.Combine(ProjectDirectory, $"{m_ProjectName}.sln");
  560. }
  561. private void ProjectHeader(StringBuilder stringBuilder, ProjectPart assembly, List<ResponseFileData> responseFilesData)
  562. {
  563. var responseFilesDataArgs = GetOtherArgumentsFromResponseFilesData(responseFilesData);
  564. stringBuilder
  565. .AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
  566. .AppendLine(
  567. "<Project ToolsVersion=\"4.0\" DefaultTargets=\"Build\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">")
  568. .AppendLine(" <PropertyGroup>")
  569. .Append(" <LangVersion>").Append(GetLangVersion(responseFilesDataArgs["langversion"], assembly)).AppendLine("</LangVersion>")
  570. .AppendLine(
  571. " <_TargetFrameworkDirectories>non_empty_path_generated_by_unity.rider.package</_TargetFrameworkDirectories>")
  572. .AppendLine(
  573. " <_FullFrameworkReferenceAssemblyPaths>non_empty_path_generated_by_unity.rider.package</_FullFrameworkReferenceAssemblyPaths>")
  574. .AppendLine(" <DisableHandlePackageFileConflicts>true</DisableHandlePackageFileConflicts>");
  575. var rulesetPaths = GetRoslynAnalyzerRulesetPaths(assembly, responseFilesDataArgs);
  576. foreach (var path in rulesetPaths)
  577. stringBuilder.Append(" <CodeAnalysisRuleSet>").Append(path).AppendLine("</CodeAnalysisRuleSet>");
  578. stringBuilder
  579. .AppendLine(" </PropertyGroup>")
  580. .AppendLine(" <PropertyGroup>")
  581. .AppendLine(" <Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>")
  582. .AppendLine(" <Platform Condition=\" '$(Platform)' == '' \">AnyCPU</Platform>")
  583. .AppendLine(" <ProductVersion>10.0.20506</ProductVersion>")
  584. .AppendLine(" <SchemaVersion>2.0</SchemaVersion>")
  585. .Append(" <RootNamespace>").Append(assembly.RootNamespace).AppendLine("</RootNamespace>")
  586. .Append(" <ProjectGuid>{").Append(ProjectGuid(m_AssemblyNameProvider.GetProjectName(assembly.Name, assembly.Defines))).AppendLine("}</ProjectGuid>")
  587. .AppendLine(
  588. " <ProjectTypeGuids>{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>")
  589. .AppendLine(" <OutputType>Library</OutputType>")
  590. .AppendLine(" <AppDesignerFolder>Properties</AppDesignerFolder>")
  591. .Append(" <AssemblyName>").Append(assembly.Name).AppendLine("</AssemblyName>")
  592. .AppendLine(" <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>")
  593. .AppendLine(" <FileAlignment>512</FileAlignment>")
  594. .AppendLine(" <BaseDirectory>.</BaseDirectory>")
  595. .AppendLine(" </PropertyGroup>")
  596. .AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' \">")
  597. .AppendLine(" <DebugSymbols>true</DebugSymbols>")
  598. .AppendLine(" <DebugType>full</DebugType>")
  599. .AppendLine(" <Optimize>false</Optimize>")
  600. .Append(" <OutputPath>").Append(assembly.OutputPath).AppendLine("</OutputPath>");
  601. var defines = new HashSet<string>(assembly.Defines);
  602. foreach (var responseFileData in responseFilesData)
  603. defines.UnionWith(responseFileData.Defines);
  604. stringBuilder
  605. .Append(" <DefineConstants>").CompatibleAppendJoin(';', defines).AppendLine("</DefineConstants>")
  606. .AppendLine(" <ErrorReport>prompt</ErrorReport>");
  607. var warningLevel = responseFilesDataArgs["warn"].Concat(responseFilesDataArgs["w"]).Distinct().FirstOrDefault();
  608. stringBuilder
  609. .Append(" <WarningLevel>").Append(!string.IsNullOrWhiteSpace(warningLevel) ? warningLevel : "4").AppendLine("</WarningLevel>")
  610. .Append(" <NoWarn>").CompatibleAppendJoin(',', GetNoWarn(responseFilesDataArgs["nowarn"].ToList())).AppendLine("</NoWarn>")
  611. .Append(" <AllowUnsafeBlocks>").Append(assembly.CompilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe)).AppendLine("</AllowUnsafeBlocks>");
  612. AppendWarningAsError(stringBuilder, responseFilesDataArgs["warnaserror"],
  613. responseFilesDataArgs["warnaserror-"], responseFilesDataArgs["warnaserror+"]);
  614. // TODO: Can we have multiple documentation files in a project file?
  615. foreach (var docFile in responseFilesDataArgs["doc"])
  616. stringBuilder.Append(" <DocumentationFile>").Append(docFile).AppendLine("</DocumentationFile>");
  617. var nullable = responseFilesDataArgs["nullable"].FirstOrDefault();
  618. if (!string.IsNullOrEmpty(nullable))
  619. stringBuilder.Append(" <Nullable>").Append(nullable).AppendLine("</Nullable>");
  620. stringBuilder
  621. .AppendLine(" </PropertyGroup>")
  622. .AppendLine(" <PropertyGroup>")
  623. .AppendLine(" <NoConfig>true</NoConfig>")
  624. .AppendLine(" <NoStdLib>true</NoStdLib>")
  625. .AppendLine(" <AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>")
  626. .AppendLine(" <ImplicitlyExpandNETStandardFacades>false</ImplicitlyExpandNETStandardFacades>")
  627. .AppendLine(" <ImplicitlyExpandDesignTimeFacades>false</ImplicitlyExpandDesignTimeFacades>")
  628. .AppendLine(" </PropertyGroup>");
  629. var analyzers = GetRoslynAnalyzers(assembly, responseFilesDataArgs);
  630. if (analyzers.Length > 0)
  631. {
  632. stringBuilder.AppendLine(" <ItemGroup>");
  633. foreach (var analyzer in analyzers)
  634. stringBuilder.AppendLine($" <Analyzer Include=\"{analyzer.NormalizePath()}\" />");
  635. stringBuilder.AppendLine(" </ItemGroup>");
  636. }
  637. var additionalFiles = GetRoslynAdditionalFiles(assembly, responseFilesDataArgs);
  638. if (additionalFiles.Length > 0)
  639. {
  640. stringBuilder.AppendLine(" <ItemGroup>");
  641. foreach (var additionalFile in additionalFiles)
  642. stringBuilder.AppendLine($" <AdditionalFiles Include=\"{additionalFile}\" />");
  643. stringBuilder.AppendLine(" </ItemGroup>");
  644. }
  645. var configFile = GetGlobalAnalyzerConfigFile(assembly);
  646. if (!string.IsNullOrEmpty(configFile))
  647. {
  648. stringBuilder
  649. .AppendLine(" <ItemGroup>")
  650. .Append(" <GlobalAnalyzerConfigFiles Include=\"").Append(configFile).AppendLine("\" />")
  651. .AppendLine(" </ItemGroup>");
  652. }
  653. }
  654. private static string GetGlobalAnalyzerConfigFile(ProjectPart assembly)
  655. {
  656. var configFile = string.Empty;
  657. #if UNITY_2021_3 // https://github.com/JetBrains/resharper-unity/issues/2401
  658. var type = assembly.CompilerOptions.GetType();
  659. var propertyInfo = type.GetProperty("AnalyzerConfigPath");
  660. if (propertyInfo != null && propertyInfo.GetValue(assembly.CompilerOptions) is string value)
  661. {
  662. configFile = value;
  663. }
  664. #elif UNITY_2022_2_OR_NEWER
  665. configFile = assembly.CompilerOptions.AnalyzerConfigPath; // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Compilation.ScriptCompilerOptions.AnalyzerConfigPath.html
  666. #endif
  667. return configFile;
  668. }
  669. private static string[] GetRoslynAdditionalFiles(ProjectPart assembly, ILookup<string, string> otherResponseFilesData)
  670. {
  671. var additionalFilePathsFromCompilationPipeline = Array.Empty<string>();
  672. #if UNITY_2021_3 // https://github.com/JetBrains/resharper-unity/issues/2401
  673. var type = assembly.CompilerOptions.GetType();
  674. var propertyInfo = type.GetProperty("RoslynAdditionalFilePaths");
  675. if (propertyInfo != null && propertyInfo.GetValue(assembly.CompilerOptions) is string[] value)
  676. {
  677. additionalFilePathsFromCompilationPipeline = value;
  678. }
  679. #elif UNITY_2022_2_OR_NEWER // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Compilation.ScriptCompilerOptions.RoslynAdditionalFilePaths.html
  680. additionalFilePathsFromCompilationPipeline = assembly.CompilerOptions.RoslynAdditionalFilePaths;
  681. #endif
  682. return otherResponseFilesData["additionalfile"]
  683. .SelectMany(x=>x.Split(';'))
  684. .Concat(additionalFilePathsFromCompilationPipeline)
  685. .Distinct().ToArray();
  686. }
  687. string[] GetRoslynAnalyzers(ProjectPart assembly, ILookup<string, string> otherResponseFilesData)
  688. {
  689. #if UNITY_2020_2_OR_NEWER
  690. return otherResponseFilesData["analyzer"].Concat(otherResponseFilesData["a"])
  691. .SelectMany(x=>x.Split(';'))
  692. #if !ROSLYN_ANALYZER_FIX
  693. .Concat(m_AssemblyNameProvider.GetRoslynAnalyzerPaths())
  694. #else
  695. .Concat(assembly.CompilerOptions.RoslynAnalyzerDllPaths)
  696. #endif
  697. .Select(GetNormalisedAssemblyPath)
  698. .Distinct()
  699. .ToArray();
  700. #else
  701. return otherResponseFilesData["analyzer"].Concat(otherResponseFilesData["a"])
  702. .SelectMany(x=>x.Split(';'))
  703. .Distinct()
  704. .Select(GetNormalisedAssemblyPath)
  705. .ToArray();
  706. #endif
  707. }
  708. private IEnumerable<string> GetRoslynAnalyzerRulesetPaths(ProjectPart assembly, ILookup<string, string> otherResponseFilesData)
  709. {
  710. var paths = new HashSet<string>(otherResponseFilesData["ruleset"]);
  711. #if UNITY_2020_2_OR_NEWER
  712. if (!string.IsNullOrEmpty(assembly.CompilerOptions.RoslynAnalyzerRulesetPath))
  713. paths.Add(assembly.CompilerOptions.RoslynAnalyzerRulesetPath);
  714. #endif
  715. return paths.Select(GetNormalisedAssemblyPath);
  716. }
  717. private static void AppendWarningAsError(StringBuilder stringBuilder,
  718. IEnumerable<string> args, IEnumerable<string> argsMinus, IEnumerable<string> argsPlus)
  719. {
  720. var treatWarningsAsErrors = false;
  721. var warningIds = new List<string>();
  722. var notWarningIds = new List<string>(argsMinus);
  723. foreach (var s in args)
  724. {
  725. if (s == "+" || s == "") treatWarningsAsErrors = true;
  726. else if (s == "-") treatWarningsAsErrors = false;
  727. else warningIds.Add(s);
  728. }
  729. warningIds.AddRange(argsPlus);
  730. stringBuilder.Append(" <TreatWarningsAsErrors>").Append(treatWarningsAsErrors) .AppendLine("</TreatWarningsAsErrors>");
  731. if (warningIds.Count > 0)
  732. stringBuilder.Append(" <WarningsAsErrors>").CompatibleAppendJoin(';', warningIds).AppendLine("</WarningsAsErrors>");
  733. if (notWarningIds.Count > 0)
  734. stringBuilder.Append(" <WarningsNotAsErrors>").CompatibleAppendJoin(';', notWarningIds) .AppendLine("</WarningsNotAsErrors>");
  735. }
  736. private void SyncSolution(StringBuilder stringBuilder, List<ProjectPart> islands, Type[] types)
  737. {
  738. SyncSolutionFileIfNotChanged(SolutionFile(), SolutionText(stringBuilder, islands), types);
  739. }
  740. private string SolutionText(StringBuilder stringBuilder, List<ProjectPart> islands)
  741. {
  742. stringBuilder
  743. .AppendLine()
  744. .AppendLine("Microsoft Visual Studio Solution File, Format Version 11.00")
  745. .AppendLine("# Visual Studio 2010");
  746. foreach (var island in islands)
  747. {
  748. var projectName = m_AssemblyNameProvider.GetProjectName(island.Name, island.Defines);
  749. // GUID is for C# class libraries
  750. stringBuilder
  751. .Append("Project(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"")
  752. .Append(island.Name)
  753. .Append("\", \"")
  754. .Append(projectName)
  755. .Append(".csproj\", \"{")
  756. .Append(ProjectGuid(projectName))
  757. .AppendLine("}\"")
  758. .AppendLine("EndProject");
  759. }
  760. stringBuilder.AppendLine("Global")
  761. .AppendLine("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution")
  762. .AppendLine("\t\tDebug|Any CPU = Debug|Any CPU")
  763. .AppendLine("\tEndGlobalSection")
  764. .AppendLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution");
  765. foreach (var island in islands)
  766. {
  767. var projectGuid = ProjectGuid(m_AssemblyNameProvider.GetProjectName(island.Name, island.Defines));
  768. stringBuilder
  769. .Append("\t\t{").Append(projectGuid).AppendLine("}.Debug|Any CPU.ActiveCfg = Debug|Any CPU")
  770. .Append("\t\t{").Append(projectGuid).AppendLine("}.Debug|Any CPU.Build.0 = Debug|Any CPU");
  771. }
  772. stringBuilder.AppendLine("\tEndGlobalSection")
  773. .AppendLine("\tGlobalSection(SolutionProperties) = preSolution")
  774. .AppendLine("\t\tHideSolutionNode = FALSE")
  775. .AppendLine("\tEndGlobalSection")
  776. .AppendLine("EndGlobal");
  777. return stringBuilder.ToString();
  778. }
  779. private static ILookup<string, string> GetOtherArgumentsFromResponseFilesData(List<ResponseFileData> responseFilesData)
  780. {
  781. var paths = responseFilesData.SelectMany(x =>
  782. {
  783. return x.OtherArguments
  784. .Where(a => a.StartsWith("/", StringComparison.Ordinal) || a.StartsWith("-", StringComparison.Ordinal))
  785. .Select(b =>
  786. {
  787. var index = b.IndexOf(":", StringComparison.Ordinal);
  788. if (index > 0 && b.Length > index)
  789. {
  790. var key = b.Substring(1, index - 1);
  791. return new KeyValuePair<string, string>(key, b.Substring(index + 1));
  792. }
  793. const string warnaserror = "warnaserror";
  794. if (b.Substring(1).StartsWith(warnaserror, StringComparison.Ordinal))
  795. {
  796. return new KeyValuePair<string, string>(warnaserror, b.Substring(warnaserror.Length + 1));
  797. }
  798. const string nullable = "nullable";
  799. if (b.Substring(1).StartsWith(nullable, StringComparison.Ordinal))
  800. {
  801. var res = b.Substring(nullable.Length + 1);
  802. if (string.IsNullOrWhiteSpace(res) || res.Equals("+"))
  803. res = "enable";
  804. else if (res.Equals("-"))
  805. res = "disable";
  806. return new KeyValuePair<string, string>(nullable, res);
  807. }
  808. return default;
  809. });
  810. })
  811. .Distinct()
  812. .ToLookup(o => o.Key, pair => pair.Value);
  813. return paths;
  814. }
  815. private string GetLangVersion(IEnumerable<string> langVersionList, ProjectPart assembly)
  816. {
  817. var langVersion = langVersionList.FirstOrDefault();
  818. if (!string.IsNullOrWhiteSpace(langVersion))
  819. return langVersion;
  820. #if UNITY_2020_2_OR_NEWER
  821. return assembly.CompilerOptions.LanguageVersion;
  822. #else
  823. return "latest";
  824. #endif
  825. }
  826. public static IEnumerable<string> GetNoWarn(List<string> codes)
  827. {
  828. #if UNITY_2019_4 || UNITY_2020_1 // RIDER-77206 Unity 2020.1.3 'PlayerSettings' does not contain a definition for 'suppressCommonWarnings'
  829. var type = typeof(PlayerSettings);
  830. var propertyInfo = type.GetProperty("suppressCommonWarnings");
  831. if (propertyInfo != null && propertyInfo.GetValue(null) is bool && (bool)propertyInfo.GetValue(null))
  832. {
  833. codes.AddRange(new[] {"0169", "0649"});
  834. }
  835. #elif UNITY_2020_2_OR_NEWER
  836. if (PlayerSettings.suppressCommonWarnings)
  837. codes.AddRange(new[] {"0169", "0649"});
  838. #endif
  839. return codes.Distinct();
  840. }
  841. private string ProjectGuid(string name)
  842. {
  843. if (!m_ProjectGuids.TryGetValue(name, out var guid))
  844. {
  845. guid = m_GUIDGenerator.ProjectGuid(m_ProjectName + name);
  846. m_ProjectGuids.Add(name, guid);
  847. }
  848. return guid;
  849. }
  850. private string GetNormalisedAssemblyPath(string path)
  851. {
  852. if (!m_NormalisedPaths.TryGetValue(path, out var normalisedPath))
  853. {
  854. normalisedPath = Path.IsPathRooted(path) ? path : Path.GetFullPath(path);
  855. normalisedPath = SecurityElement.Escape(normalisedPath).NormalizePath();
  856. m_NormalisedPaths.Add(path, normalisedPath);
  857. }
  858. return normalisedPath;
  859. }
  860. private string GetAssemblyNameFromPath(string path)
  861. {
  862. if (!m_AssemblyNames.TryGetValue(path, out var name))
  863. {
  864. name = FileSystemUtil.FileNameWithoutExtension(path);
  865. m_AssemblyNames.Add(path, name);
  866. }
  867. return name;
  868. }
  869. }
  870. internal class FilePathTrie<TData>
  871. {
  872. private static readonly char[] Separators = { '\\', '/' };
  873. private readonly TrieNode m_Root = new TrieNode();
  874. private class TrieNode
  875. {
  876. public Dictionary<string, TrieNode> Children;
  877. public TData Data;
  878. }
  879. public void Insert(string filePath, TData data)
  880. {
  881. var parts = filePath.Split(Separators);
  882. var node = m_Root;
  883. foreach (var part in parts)
  884. {
  885. if (node.Children == null)
  886. node.Children = new Dictionary<string, TrieNode>(StringComparer.OrdinalIgnoreCase);
  887. // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd
  888. if (!node.Children.ContainsKey(part))
  889. node.Children[part] = new TrieNode();
  890. node = node.Children[part];
  891. }
  892. node.Data = data;
  893. }
  894. public TData FindClosestMatch(string filePath)
  895. {
  896. var parts = filePath.Split(Separators);
  897. var node = m_Root;
  898. foreach (var part in parts)
  899. {
  900. if (node.Children != null && node.Children.TryGetValue(part, out var next))
  901. node = next;
  902. else
  903. break;
  904. }
  905. return node.Data;
  906. }
  907. }
  908. }