BITFALL/Assets/Plugins/FastScriptReload/Scripts/Editor/Compilation/DynamicCompilationBase.cs

316 lines
16 KiB
C#
Raw Normal View History

2023-06-08 14:09:50 +08:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using FastScriptReload.Editor.Compilation.CodeRewriting;
using FastScriptReload.Editor.Compilation.ScriptGenerationOverrides;
using FastScriptReload.Runtime;
using FastScriptReload.Scripts.Runtime;
using ImmersiveVRTools.Editor.Common.Cache;
using ImmersiveVRTools.Runtime.Common.Utilities;
using ImmersiveVrToolsCommon.Runtime.Logging;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
namespace FastScriptReload.Editor.Compilation
{
[InitializeOnLoad]
public class DynamicCompilationBase
{
public static bool DebugWriteRewriteReasonAsComment;
public static bool LogHowToFixMessageOnCompilationError;
public static bool EnableExperimentalThisCallLimitationFix;
public static List<string> ReferencesExcludedFromHotReload = new List<string>();
public const string DebuggingInformationComment =
@"// DEBUGGING READ-ME " +
#if !UNITY_2021_1_OR_NEWER
"WARN: on Unity versions prior to 2021, opening files in that manner can cause static values to be reinitialized"
#else
""
#endif
+
@"//
// To debug simply add a breakpoint in this file.
//
// With every code change - new file is generated, currently you'll need to re-set breakpoints after each change.
// You can also:
// - step into the function that was changed (and that will get you to correct source file)
// - add a function breakpoint in your IDE (this way you won't have to re-add it every time)
//
// Tool can automatically open dynamically-compiled code file every time to make setting breakpoints easier.
// You can adjust that behaviour via 'Window -> FastScriptReload -> Start Screen -> Debugging -> Do not auto-open generated cs file'.
//
// You can always open generated file when needed by clicking link in console, eg.
// 'FSR: Files: FunctionLibrary.cs changed (click here to debug [in bottom details pane]) - compilation (took 240ms)'
";
public static readonly string[] ActiveScriptCompilationDefines;
protected static readonly string DynamicallyCreatedAssemblyAttributeSourceCode = $"[assembly: {typeof(DynamicallyCreatedAssemblyAttribute).FullName}()]";
private static readonly string AssemblyCsharpFullPath;
static DynamicCompilationBase()
{
//needs to be set from main thread
ActiveScriptCompilationDefines = EditorUserBuildSettings.activeScriptCompilationDefines;
AssemblyCsharpFullPath = SessionStateCache.GetOrCreateString(
$"FSR:AssemblyCsharpFullPath",
() => AssetDatabase.FindAssets("Microsoft.CSharp")
.Select(g => new System.IO.FileInfo(UnityEngine.Application.dataPath + "/../" + AssetDatabase.GUIDToAssetPath(g)))
.First(fi => fi.Name.ToLower() == "Microsoft.CSharp.dll".ToLower()).FullName
);
}
protected static string CreateSourceCodeCombinedContents(List<string> sourceCodeFiles, List<string> definedPreprocessorSymbols)
{
var combinedUsingStatements = new List<string>();
var sourceCodeWithAdjustments = sourceCodeFiles.Select(sourceCodeFile =>
{
var fileCode = File.ReadAllText(sourceCodeFile);
var tree = CSharpSyntaxTree.ParseText(fileCode, new CSharpParseOptions(preprocessorSymbols: definedPreprocessorSymbols));
var root = tree.GetRoot();
var typeToNewFieldDeclarations = new Dictionary<string, List<string>>();
if (FastScriptReloadManager.Instance.AssemblyChangesLoaderEditorOptionsNeededInBuild.EnableExperimentalAddedFieldsSupport)
{
//WARN: needs to walk before root class name changes, otherwise it'll resolve wrong name
var fieldsWalker = new FieldsWalker();
fieldsWalker.Visit(root);
var typeToFieldDeclarations = fieldsWalker.GetTypeToFieldDeclarations();
typeToNewFieldDeclarations = typeToFieldDeclarations.ToDictionary(
t => t.Key,
t =>
{
if (!ProjectTypeCache.AllTypesInNonDynamicGeneratedAssemblies.TryGetValue(t.Key, out var existingType))
{
LoggerScoped.LogDebug($"Unable to find type: {t.Key} in loaded assemblies. If that's the class you've added field to then it may not be properly working. It's possible the class was not yet loaded / used and you can ignore that warning. If it's causing any issues please contact support");
return new List<string>();
}
var existingTypeMembersToReplace = NewFieldsRewriter.GetReplaceableMembers(existingType).Select(m => m.Name).ToList();
var newFields = t.Value.Where(fD => !existingTypeMembersToReplace.Contains(fD.FieldName)).ToList();
//TODO: ideally that registration would happen outside of this class
//TODO: to work for LSR it needs to be handled in runtime
TemporaryNewFieldValues.RegisterNewFields(
existingType,
newFields.ToDictionary(
fD => fD.FieldName,
fD => new TemporaryNewFieldValues.GetNewFieldInitialValue((Type forNewlyGeneratedType) =>
{
//TODO: PERF: could cache those - they run to init every new value (for every instance when accessed)
return CreateNewFieldInitMethodRewriter.ResolveNewFieldsToCreateValueFn(forNewlyGeneratedType)[fD.FieldName]();
})
),
newFields.ToDictionary(
fD => fD.FieldName,
fD => new TemporaryNewFieldValues.GetNewFieldType((Type forNewlyGeneratedType) =>
{
//TODO: PERF: could cache those - they run to init every new value (for every instance when accessed)
return (Type)CreateNewFieldInitMethodRewriter.ResolveNewFieldsToTypeFn(forNewlyGeneratedType)[fD.FieldName]();
})
)
);
return newFields.Select(fD => fD.FieldName).ToList();
}
);
#if LiveScriptReload_Enabled
if (typeToNewFieldDeclarations.Any(kv => kv.Value.Any()))
{
LoggerScoped.LogWarning($"{nameof(FastScriptReloadManager.Instance.AssemblyChangesLoaderEditorOptionsNeededInBuild.EnableExperimentalAddedFieldsSupport)} is enabled. This is not supported in running build. Quite likely it'll crash remote client.");
}
#endif
}
//WARN: application order is important, eg ctors need to happen before class names as otherwise ctors will not be recognised as ctors
if (FastScriptReloadManager.Instance.EnableExperimentalThisCallLimitationFix)
{
root = new ThisCallRewriter(DebugWriteRewriteReasonAsComment).Visit(root);
root = new ThisAssignmentRewriter(DebugWriteRewriteReasonAsComment).Visit(root);
}
if (FastScriptReloadManager.Instance.AssemblyChangesLoaderEditorOptionsNeededInBuild.EnableExperimentalAddedFieldsSupport)
{
root = new NewFieldsRewriter(typeToNewFieldDeclarations, DebugWriteRewriteReasonAsComment).Visit(root);
root = new CreateNewFieldInitMethodRewriter(typeToNewFieldDeclarations, DebugWriteRewriteReasonAsComment).Visit(root);
}
root = new ConstructorRewriter(adjustCtorOnlyForNonNestedTypes: true, DebugWriteRewriteReasonAsComment).Visit(root);
var hotReloadCompliantRewriter = new HotReloadCompliantRewriter(DebugWriteRewriteReasonAsComment);
root = hotReloadCompliantRewriter.Visit(root);
combinedUsingStatements.AddRange(hotReloadCompliantRewriter.StrippedUsingDirectives);
//processed as last step to simply rewrite all changes made before
if (TryResolveUserDefinedOverridesRoot(sourceCodeFile, definedPreprocessorSymbols, out var userDefinedOverridesRoot))
{
root = ProcessUserDefinedOverridesReplacements(sourceCodeFile, root, userDefinedOverridesRoot);
root = AddUserDefinedOverridenTypes(userDefinedOverridesRoot, root);
}
return root.ToFullString();
}).ToList();
var sourceCodeCombinedSb = new StringBuilder();
sourceCodeCombinedSb.Append(DebuggingInformationComment);
foreach (var usingStatement in combinedUsingStatements.Distinct())
{
sourceCodeCombinedSb.Append(usingStatement);
}
foreach (var sourceCodeWithAdjustment in sourceCodeWithAdjustments)
{
sourceCodeCombinedSb.AppendLine(sourceCodeWithAdjustment);
}
LoggerScoped.LogDebug("Source Code Created:\r\n\r\n" + sourceCodeCombinedSb);
return sourceCodeCombinedSb.ToString();
}
private static SyntaxNode AddUserDefinedOverridenTypes(SyntaxNode userDefinedOverridesRoot, SyntaxNode root)
{
try
{
var userDefinedOverrideTypes = userDefinedOverridesRoot.DescendantNodes().OfType<TypeDeclarationSyntax>()
.ToDictionary(n => RoslynUtils.GetMemberFQDN(n, n.Identifier.ToString()));
var allDefinedTypesInRecompiledFile = root.DescendantNodes().OfType<TypeDeclarationSyntax>()
.ToDictionary(n => RoslynUtils.GetMemberFQDN(n, n.Identifier.ToString())); //what about nested types?
var userDefinedOverrideTypesWithoutMatchnigInRecompiledFile = userDefinedOverrideTypes.Select(overridenType =>
{
if (!allDefinedTypesInRecompiledFile.ContainsKey(overridenType.Key))
{
return overridenType;
}
return default(KeyValuePair<string, TypeDeclarationSyntax>);
})
.Where(kv => kv.Key != default(string))
.ToList();
//types should be added either to root namespace or root of document
var rootNamespace = root.DescendantNodes().OfType<NamespaceDeclarationSyntax>().FirstOrDefault();
foreach (var overridenTypeToAdd in userDefinedOverrideTypesWithoutMatchnigInRecompiledFile)
{
var newMember = FastScriptReloadCodeRewriterBase.AddRewriteCommentIfNeeded(overridenTypeToAdd.Value,
"New type defined in override file",
true, //always write reason so it's not easy to miss in generated file
true);
if (rootNamespace != null)
{
rootNamespace =
root.DescendantNodes().OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault(); //need to search again to make sure it didn't change
var newRootNamespace = rootNamespace.AddMembers(newMember);
root = root.ReplaceNode(rootNamespace, newRootNamespace);
}
else
{
root = ((CompilationUnitSyntax)root).AddMembers(newMember);
}
}
}
catch (Exception e)
{
Debug.LogError($"Unable to add user defined override types. {e}");
}
return root;
}
private static bool TryResolveUserDefinedOverridesRoot(string sourceCodeFile, List<string> definedPreprocessorSymbols, out SyntaxNode userDefinedOverridesRoot)
{
if (ScriptGenerationOverridesManager.TryGetScriptOverride(new FileInfo(sourceCodeFile), out var userDefinedOverridesFile))
{
try
{
userDefinedOverridesRoot = CSharpSyntaxTree.ParseText(File.ReadAllText(userDefinedOverridesFile.FullName), new CSharpParseOptions(preprocessorSymbols: definedPreprocessorSymbols)).GetRoot();
return true;
}
catch (Exception ex)
{
Debug.LogError($"Unable to resolve user defined overrides for file: '{userDefinedOverridesFile.FullName}' - please make sure it's compilable. Error: '{ex}'");
}
}
userDefinedOverridesRoot = null;
return false;
}
private static SyntaxNode ProcessUserDefinedOverridesReplacements(string sourceCodeFile, SyntaxNode root, SyntaxNode userDefinedOverridesRoot)
{
if (ScriptGenerationOverridesManager.TryGetScriptOverride(new FileInfo(sourceCodeFile), out var userDefinedOverridesFile))
{
try
{
var userDefinedScriptOverridesRewriter = new ManualUserDefinedScriptOverridesRewriter(userDefinedOverridesRoot,
true); //always write rewrite reason so it's not easy to miss
root = userDefinedScriptOverridesRewriter.Visit(root);
}
catch (Exception ex)
{
Debug.LogError($"Unable to resolve user defined overrides for file: '{userDefinedOverridesFile.FullName}' - please make sure it's compilable. Error: '{ex}'");
}
}
return root;
}
protected static List<string> ResolveReferencesToAdd(List<string> excludeAssyNames)
{
var referencesToAdd = new List<string>();
foreach (var assembly in AppDomain.CurrentDomain
.GetAssemblies() //TODO: PERF: just need to load once and cache? or get assembly based on changed file only?
.Where(a => excludeAssyNames.All(assyName => !a.FullName.StartsWith(assyName))
&& CustomAttributeExtensions.GetCustomAttribute<DynamicallyCreatedAssemblyAttribute>((Assembly)a) == null))
{
try
{
if (string.IsNullOrEmpty(assembly.Location))
{
LoggerScoped.LogDebug($"FastScriptReload: Assembly location is null, usually dynamic assembly, harmless.");
continue;
}
referencesToAdd.Add(assembly.Location);
}
catch (Exception)
{
LoggerScoped.LogDebug($"Unable to add a reference to assembly as unable to get location or null: {assembly.FullName} when hot-reloading, this is likely dynamic assembly and won't cause issues");
}
}
referencesToAdd = referencesToAdd.Where(r => !ReferencesExcludedFromHotReload.Any(rTe => r.EndsWith(rTe))).ToList();
if (EnableExperimentalThisCallLimitationFix || FastScriptReloadManager.Instance.AssemblyChangesLoaderEditorOptionsNeededInBuild.EnableExperimentalAddedFieldsSupport)
{
IncludeMicrosoftCsharpReferenceToSupportDynamicKeyword(referencesToAdd);
}
return referencesToAdd;
}
private static void IncludeMicrosoftCsharpReferenceToSupportDynamicKeyword(List<string> referencesToAdd)
{
//TODO: check .net4.5 backend not breaking?
//ThisRewriters will cast to dynamic - if using .NET Standard 2.1 - reference is required
referencesToAdd.Add(AssemblyCsharpFullPath);
// referencesToAdd.Add(@"C:\Program Files\Unity\Hub\Editor\2021.3.12f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Microsoft.CSharp.dll");
}
}
}