mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-15 03:20:30 +00:00
Replace manual dynamic compilation with .NET Hot Reload
This commit is contained in:
@@ -21,7 +21,6 @@
|
||||
<ItemGroup Label="Code Analysis">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3" PrivateAssets="All" />
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Code Analysis">
|
||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu-framework.ruleset</CodeAnalysisRuleSet>
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using System.Reflection.Metadata;
|
||||
using osu.Framework.Testing;
|
||||
#endif
|
||||
|
||||
// We publish our internal attributes to other sub-projects of the framework.
|
||||
// Note, that we omit visual tests as they are meant to test the framework
|
||||
// behavior "in the wild".
|
||||
@@ -13,3 +18,7 @@ using System.Runtime.CompilerServices;
|
||||
[assembly: InternalsVisibleTo("osu.Framework.Tests.Dynamic")]
|
||||
[assembly: InternalsVisibleTo("osu.Framework.Tests.iOS")]
|
||||
[assembly: InternalsVisibleTo("osu.Framework.Tests.Android")]
|
||||
|
||||
#if NET6_0
|
||||
[assembly: MetadataUpdateHandler(typeof(DynamicClassCompilerStatics))]
|
||||
#endif
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace osu.Framework.Testing
|
||||
{
|
||||
internal readonly struct AssemblyReference
|
||||
{
|
||||
public readonly Assembly Assembly;
|
||||
public readonly bool IgnoreAccessChecks;
|
||||
|
||||
public AssemblyReference(Assembly assembly, bool ignoreAccessChecks)
|
||||
{
|
||||
Assembly = assembly;
|
||||
IgnoreAccessChecks = ignoreAccessChecks;
|
||||
}
|
||||
|
||||
public MetadataReference GetReference() => MetadataReference.CreateFromFile(Assembly.Location);
|
||||
}
|
||||
}
|
||||
@@ -2,290 +2,30 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using osu.Framework.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace osu.Framework.Testing
|
||||
{
|
||||
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
|
||||
internal class DynamicClassCompiler<T> : IDisposable
|
||||
where T : IDynamicallyCompile
|
||||
public static class DynamicClassCompilerStatics
|
||||
{
|
||||
public event Action CompilationStarted;
|
||||
public static event Action<Type[]> CompilationFinished;
|
||||
|
||||
public static void UpdateApplication([CanBeNull] Type[] updatedTypes) => CompilationFinished?.Invoke(updatedTypes);
|
||||
}
|
||||
|
||||
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
|
||||
internal class DynamicClassCompiler<T>
|
||||
{
|
||||
public event Action<Type> CompilationFinished;
|
||||
|
||||
public event Action<Exception> CompilationFailed;
|
||||
|
||||
private readonly List<FileSystemWatcher> watchers = new List<FileSystemWatcher>();
|
||||
private readonly HashSet<string> requiredFiles = new HashSet<string>();
|
||||
|
||||
private T target;
|
||||
|
||||
public void SetRecompilationTarget(T target)
|
||||
{
|
||||
if (this.target?.GetType().Name != target?.GetType().Name)
|
||||
{
|
||||
requiredFiles.Clear();
|
||||
referenceBuilder.Reset();
|
||||
}
|
||||
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
private ITypeReferenceBuilder referenceBuilder;
|
||||
public void SetRecompilationTarget(T target) => this.target = target;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
referenceBuilder = new EmptyTypeReferenceBuilder();
|
||||
|
||||
Logger.Log("Dynamic compilation disabled (debugger attached).");
|
||||
return;
|
||||
}
|
||||
|
||||
var di = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
|
||||
string basePath = getSolutionPath(di);
|
||||
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
referenceBuilder = new EmptyTypeReferenceBuilder();
|
||||
|
||||
Logger.Log("Dynamic compilation disabled (no solution file found).");
|
||||
return;
|
||||
}
|
||||
|
||||
#if NET6_0
|
||||
referenceBuilder = new RoslynTypeReferenceBuilder();
|
||||
#else
|
||||
referenceBuilder = new EmptyTypeReferenceBuilder();
|
||||
#endif
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
Logger.Log("Initialising dynamic compilation...");
|
||||
|
||||
await referenceBuilder.Initialise(Directory.GetFiles(basePath, "*.sln").First()).ConfigureAwait(false);
|
||||
|
||||
foreach (string dir in Directory.GetDirectories(basePath))
|
||||
{
|
||||
// only watch directories which house a csproj. this avoids submodules and directories like .git which can contain many files.
|
||||
if (!Directory.GetFiles(dir, "*.csproj").Any())
|
||||
continue;
|
||||
|
||||
var fsw = new FileSystemWatcher(dir, @"*.cs")
|
||||
{
|
||||
EnableRaisingEvents = true,
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.FileName,
|
||||
};
|
||||
|
||||
fsw.Renamed += onChange;
|
||||
fsw.Changed += onChange;
|
||||
fsw.Created += onChange;
|
||||
|
||||
watchers.Add(fsw);
|
||||
}
|
||||
|
||||
Logger.Log("Dynamic compilation is now available.");
|
||||
});
|
||||
DynamicClassCompilerStatics.CompilationFinished += types => CompilationFinished?.Invoke(target.GetType());
|
||||
}
|
||||
|
||||
private static string getSolutionPath(DirectoryInfo d)
|
||||
{
|
||||
if (d == null)
|
||||
return null;
|
||||
|
||||
return d.GetFiles().Any(f => f.Extension == ".sln") ? d.FullName : getSolutionPath(d.Parent);
|
||||
}
|
||||
|
||||
private void onChange(object sender, FileSystemEventArgs args) => Task.Run(async () => await recompileAsync(target?.GetType(), args.FullPath).ConfigureAwait(false));
|
||||
|
||||
private int currentVersion;
|
||||
private bool isCompiling;
|
||||
|
||||
private async Task recompileAsync(Type targetType, string changedFile)
|
||||
{
|
||||
if (targetType == null || isCompiling || referenceBuilder is EmptyTypeReferenceBuilder)
|
||||
return;
|
||||
|
||||
isCompiling = true;
|
||||
|
||||
try
|
||||
{
|
||||
while (!checkFileReady(changedFile))
|
||||
Thread.Sleep(10);
|
||||
|
||||
Logger.Log($@"Recompiling {Path.GetFileName(targetType.Name)}...", LoggingTarget.Runtime, LogLevel.Important);
|
||||
|
||||
CompilationStarted?.Invoke();
|
||||
|
||||
foreach (string f in await referenceBuilder.GetReferencedFiles(targetType, changedFile).ConfigureAwait(false))
|
||||
requiredFiles.Add(f);
|
||||
|
||||
var assemblies = await referenceBuilder.GetReferencedAssemblies(targetType, changedFile).ConfigureAwait(false);
|
||||
|
||||
using (var pdbStream = new MemoryStream())
|
||||
using (var peStream = new MemoryStream())
|
||||
{
|
||||
var compilationResult = createCompilation(targetType, requiredFiles, assemblies).Emit(peStream, pdbStream);
|
||||
|
||||
if (compilationResult.Success)
|
||||
{
|
||||
peStream.Seek(0, SeekOrigin.Begin);
|
||||
pdbStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
CompilationFinished?.Invoke(
|
||||
Assembly.Load(peStream.ToArray(), pdbStream.ToArray()).GetModules()[0].GetTypes().LastOrDefault(t => t.FullName == targetType.FullName)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
foreach (var diagnostic in compilationResult.Diagnostics)
|
||||
{
|
||||
if (diagnostic.Severity < DiagnosticSeverity.Error)
|
||||
continue;
|
||||
|
||||
exceptions.Add(new InvalidOperationException(diagnostic.ToString()));
|
||||
}
|
||||
|
||||
throw new AggregateException(exceptions.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CompilationFailed?.Invoke(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isCompiling = false;
|
||||
}
|
||||
}
|
||||
|
||||
private CSharpCompilationOptions createCompilationOptions()
|
||||
{
|
||||
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||
.WithMetadataImportOptions(MetadataImportOptions.Internal);
|
||||
|
||||
// This is an internal property which allows the compiler to ignore accessibility checks.
|
||||
// https://www.strathweb.com/2018/10/no-internalvisibleto-no-problem-bypassing-c-visibility-rules-with-roslyn/
|
||||
var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Debug.Assert(topLevelBinderFlagsProperty != null);
|
||||
topLevelBinderFlagsProperty.SetValue(options, (uint)1 << 22);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private CSharpCompilation createCompilation(Type targetType, IEnumerable<string> files, IEnumerable<AssemblyReference> assemblies)
|
||||
{
|
||||
// ReSharper disable once RedundantExplicitArrayCreation this doesn't compile when the array is empty
|
||||
var parseOptions = new CSharpParseOptions(preprocessorSymbols: new string[]
|
||||
{
|
||||
#if DEBUG
|
||||
"DEBUG",
|
||||
#endif
|
||||
#if TRACE
|
||||
"TRACE",
|
||||
#endif
|
||||
#if RELEASE
|
||||
"RELEASE",
|
||||
#endif
|
||||
}, languageVersion: LanguageVersion.Latest);
|
||||
|
||||
// Add the syntax trees for all referenced files.
|
||||
var syntaxTrees = new List<SyntaxTree>();
|
||||
foreach (string f in files)
|
||||
syntaxTrees.Add(CSharpSyntaxTree.ParseText(File.ReadAllText(f, Encoding.UTF8), parseOptions, f, encoding: Encoding.UTF8));
|
||||
|
||||
// Add the new assembly version, such that it replaces any existing dynamic assembly.
|
||||
string assemblyVersion = $"{++currentVersion}.0.*";
|
||||
syntaxTrees.Add(CSharpSyntaxTree.ParseText($"using System.Reflection; [assembly: AssemblyVersion(\"{assemblyVersion}\")]", parseOptions));
|
||||
|
||||
// Add a custom compiler attribute to allow ignoring access checks.
|
||||
syntaxTrees.Add(CSharpSyntaxTree.ParseText(ignores_access_checks_to_attribute_syntax, parseOptions));
|
||||
|
||||
// Ignore access checks for assemblies that have had their internal types referenced.
|
||||
var ignoreAccessChecksText = new StringBuilder();
|
||||
ignoreAccessChecksText.AppendLine("using System.Runtime.CompilerServices;");
|
||||
foreach (var asm in assemblies.Where(asm => asm.IgnoreAccessChecks))
|
||||
ignoreAccessChecksText.AppendLine($"[assembly: IgnoresAccessChecksTo(\"{asm.Assembly.GetName().Name}\")]");
|
||||
syntaxTrees.Add(CSharpSyntaxTree.ParseText(ignoreAccessChecksText.ToString(), parseOptions));
|
||||
|
||||
// Determine the new assembly name, ensuring that the dynamic suffix is not duplicated.
|
||||
string assemblyNamespace = targetType.Assembly.GetName().Name?.Replace(".Dynamic", "");
|
||||
string dynamicNamespace = $"{assemblyNamespace}.Dynamic";
|
||||
|
||||
return CSharpCompilation.Create(
|
||||
dynamicNamespace,
|
||||
syntaxTrees,
|
||||
assemblies.Select(asm => asm.GetReference()),
|
||||
createCompilationOptions()
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a file has finished being written to.
|
||||
/// </summary>
|
||||
private static bool checkFileReady(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream inputStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None))
|
||||
return inputStream.Length > 0;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!isDisposed)
|
||||
{
|
||||
isDisposed = true;
|
||||
watchers.ForEach(w => w.Dispose());
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private const string ignores_access_checks_to_attribute_syntax =
|
||||
@"namespace System.Runtime.CompilerServices
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public class IgnoresAccessChecksToAttribute : Attribute
|
||||
{
|
||||
public IgnoresAccessChecksToAttribute(string assemblyName)
|
||||
{
|
||||
AssemblyName = assemblyName;
|
||||
}
|
||||
public string AssemblyName { get; }
|
||||
}
|
||||
}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace osu.Framework.Testing
|
||||
{
|
||||
internal class EmptyTypeReferenceBuilder : ITypeReferenceBuilder
|
||||
{
|
||||
public Task Initialise(string solutionFile) => Task.CompletedTask;
|
||||
|
||||
public async Task<IReadOnlyCollection<string>> GetReferencedFiles(Type testType, string changedFile)
|
||||
=> await Task.FromResult(Array.Empty<string>()).ConfigureAwait(false);
|
||||
|
||||
public async Task<IReadOnlyCollection<AssemblyReference>> GetReferencedAssemblies(Type testType, string changedFile)
|
||||
=> await Task.FromResult(Array.Empty<AssemblyReference>()).ConfigureAwait(false);
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Framework.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// A class which can be recompiled at runtime to allow for rapid testing.
|
||||
/// </summary>
|
||||
internal interface IDynamicallyCompile
|
||||
{
|
||||
/// <summary>
|
||||
/// A reference to the original instance which dynamic compilation was based on.
|
||||
/// Will reference self if already the original.
|
||||
/// </summary>
|
||||
object DynamicCompilationOriginal { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
|
||||
namespace osu.Framework.Testing
|
||||
{
|
||||
internal interface ITypeReferenceBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialises this <see cref="ITypeReferenceBuilder"/> with a given solution file.
|
||||
/// </summary>
|
||||
/// <param name="solutionFile">The solution file.</param>
|
||||
Task Initialise(string solutionFile);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all files referenced by the type hierarchy joining a given <see cref="Type"/> to a given file.
|
||||
/// </summary>
|
||||
/// <param name="testType">The <see cref="Type"/>.</param>
|
||||
/// <param name="changedFile">The file.</param>
|
||||
/// <returns>The file names containing all types referenced between <paramref name="testType"/> and <paramref name="changedFile"/>.</returns>
|
||||
Task<IReadOnlyCollection<string>> GetReferencedFiles(Type testType, string changedFile);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all assemblies referenced by the type hierarchy joining a given <see cref="Type"/> to a given file.
|
||||
/// </summary>
|
||||
/// <param name="testType">The <see cref="Type"/>.</param>
|
||||
/// <param name="changedFile">The file.</param>
|
||||
/// <returns>The file names containing all assemblies referenced between <paramref name="testType"/> and <paramref name="changedFile"/>.</returns>
|
||||
Task<IReadOnlyCollection<AssemblyReference>> GetReferencedAssemblies(Type testType, string changedFile);
|
||||
|
||||
/// <summary>
|
||||
/// Resets this <see cref="ITypeReferenceBuilder"/>.
|
||||
/// </summary>
|
||||
void Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that there was no link between a given test type and the changed file.
|
||||
/// </summary>
|
||||
internal class NoLinkBetweenTypesException : Exception
|
||||
{
|
||||
public NoLinkBetweenTypesException(Type testType, string changedFile)
|
||||
: base($"The changed file \"{Path.GetFileName(changedFile)}\" is not used by the test \"{testType.ReadableName()}\".")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,722 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#if NET6_0
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Build.Locator;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.MSBuild;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Framework.Testing
|
||||
{
|
||||
internal class RoslynTypeReferenceBuilder : ITypeReferenceBuilder
|
||||
{
|
||||
// The "Attribute" suffix disappears when used via a nuget package, so it is trimmed here.
|
||||
private static readonly string exclude_attribute_name = nameof(ExcludeFromDynamicCompileAttribute).Replace(nameof(Attribute), string.Empty);
|
||||
|
||||
private static readonly string[] banned_assemblies =
|
||||
{
|
||||
"Microsoft.NET.Build.Tasks"
|
||||
};
|
||||
|
||||
// Some types are special reflection-only markers which are defined across multiple libraries to take advantage of tooling.
|
||||
// Including assemblies containing these types would cause type conflicts since we're exposing all internal types to the dynamic class.
|
||||
private static readonly string[] banned_types =
|
||||
{
|
||||
"JetBrains.Annotations"
|
||||
};
|
||||
|
||||
private readonly Logger logger;
|
||||
|
||||
private readonly ConcurrentDictionary<TypeReference, IReadOnlyCollection<TypeReference>> referenceMap = new ConcurrentDictionary<TypeReference, IReadOnlyCollection<TypeReference>>();
|
||||
private readonly ConcurrentDictionary<Project, Compilation> compilationCache = new ConcurrentDictionary<Project, Compilation>();
|
||||
private readonly ConcurrentDictionary<string, SemanticModel> semanticModelCache = new ConcurrentDictionary<string, SemanticModel>();
|
||||
private readonly ConcurrentDictionary<TypeReference, bool> typeInheritsFromGameCache = new ConcurrentDictionary<TypeReference, bool>();
|
||||
private readonly ConcurrentDictionary<string, bool> syntaxExclusionMap = new ConcurrentDictionary<string, bool>();
|
||||
private readonly ConcurrentDictionary<string, byte> assembliesContainingReferencedInternalMembers = new ConcurrentDictionary<string, byte>();
|
||||
|
||||
private Solution solution;
|
||||
|
||||
public RoslynTypeReferenceBuilder()
|
||||
{
|
||||
logger = Logger.GetLogger("dynamic-compilation");
|
||||
logger.OutputToListeners = false;
|
||||
}
|
||||
|
||||
public async Task Initialise(string solutionFile)
|
||||
{
|
||||
MSBuildLocator.RegisterDefaults();
|
||||
solution = await MSBuildWorkspace.Create().OpenSolutionAsync(solutionFile).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<string>> GetReferencedFiles(Type testType, string changedFile)
|
||||
{
|
||||
clearCaches();
|
||||
updateFile(changedFile);
|
||||
|
||||
await buildReferenceMapAsync(testType, changedFile).ConfigureAwait(false);
|
||||
|
||||
var sources = getTypesFromFile(changedFile).ToArray();
|
||||
if (sources.Length == 0)
|
||||
throw new NoLinkBetweenTypesException(testType, changedFile);
|
||||
|
||||
return getReferencedFiles(sources, getDirectedGraph());
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<AssemblyReference>> GetReferencedAssemblies(Type testType, string changedFile) => await Task.Run(() =>
|
||||
{
|
||||
// Todo: This is temporary, and is potentially missing assemblies.
|
||||
|
||||
var assemblies = new HashSet<AssemblyReference>();
|
||||
|
||||
foreach (var asm in compilationCache.Values.SelectMany(c => c.ReferencedAssemblyNames))
|
||||
addReference(Assembly.Load(asm.Name), false);
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic))
|
||||
addReference(asm, false);
|
||||
addReference(typeof(JetBrains.Annotations.NotNullAttribute).Assembly, true);
|
||||
|
||||
return assemblies;
|
||||
|
||||
void addReference(Assembly assembly, bool force)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assembly.Location))
|
||||
return;
|
||||
|
||||
if (!force && banned_assemblies.Any(banned => assembly.FullName?.Contains(banned) == true))
|
||||
return;
|
||||
|
||||
Type[] loadedTypes = assembly.GetLoadableTypes();
|
||||
|
||||
if (!force && banned_types.Any(banned => loadedTypes.Any(t => t.FullName?.Contains(banned) == true)))
|
||||
return;
|
||||
|
||||
bool containsReferencedInternalMember = assembliesContainingReferencedInternalMembers.Any(i => assembly.FullName?.Contains(i.Key) == true);
|
||||
assemblies.Add(new AssemblyReference(assembly, containsReferencedInternalMember));
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
clearCaches();
|
||||
referenceMap.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the reference map, connecting all types to their immediate references. Results are placed inside <see cref="referenceMap"/>.
|
||||
/// </summary>
|
||||
/// <param name="testType">The test target - the top-most level.</param>
|
||||
/// <param name="changedFile">The file that was changed.</param>
|
||||
/// <exception cref="InvalidOperationException">If <paramref name="testType"/> could not be retrieved from the solution.</exception>
|
||||
private async Task buildReferenceMapAsync(Type testType, string changedFile)
|
||||
{
|
||||
// We want to find a graph of types from the testType symbol (P) to all the types which it references recursively.
|
||||
//
|
||||
// P
|
||||
// / \
|
||||
// / \
|
||||
// / \
|
||||
// C1 C2 ---
|
||||
// / \ | /
|
||||
// C3 C4 C5 /
|
||||
// \ / /
|
||||
// C6 ---
|
||||
//
|
||||
// The reference map is a key-value pairing of all types to their immediate references. A directed graph can be built by traversing through types.
|
||||
//
|
||||
// P -> { C1, C2 }
|
||||
// C1 -> { C3, C4 }
|
||||
// C2 -> { C5, C6 }
|
||||
// C3 -> { }
|
||||
// C4 -> { C6 }
|
||||
// C5 -> { C6 }
|
||||
// C6 -> { C2 }
|
||||
|
||||
logger.Add("Building reference map...");
|
||||
|
||||
var compiledTestProject = await compileProjectAsync(findTestProject()).ConfigureAwait(false);
|
||||
var compiledTestType = compiledTestProject.GetTypeByMetadataName(testType.FullName);
|
||||
|
||||
if (compiledTestType == null)
|
||||
throw new InvalidOperationException("Failed to retrieve test type from the solution.");
|
||||
|
||||
if (referenceMap.Count > 0)
|
||||
{
|
||||
logger.Add("Attempting to use cache...");
|
||||
|
||||
// We already have some references, so we can do a partial re-process of the map for only the changed file.
|
||||
var oldTypes = getTypesFromFile(changedFile).ToArray();
|
||||
|
||||
foreach (var t in oldTypes)
|
||||
{
|
||||
referenceMap.TryRemove(t, out _);
|
||||
typeInheritsFromGameCache.TryRemove(t, out _);
|
||||
}
|
||||
|
||||
foreach (var t in oldTypes)
|
||||
{
|
||||
string typePath = t.Symbol.Locations.First().SourceTree?.FilePath;
|
||||
|
||||
// The type we have is on an old compilation, we need to re-retrieve it on the new one.
|
||||
var project = getProjectFromFile(typePath);
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
logger.Add("File has been renamed. Rebuilding reference map from scratch...");
|
||||
Reset();
|
||||
break;
|
||||
}
|
||||
|
||||
var compilation = await compileProjectAsync(project).ConfigureAwait(false);
|
||||
var syntaxTree = compilation.SyntaxTrees.Single(tree => tree.FilePath == typePath);
|
||||
var semanticModel = await getSemanticModelAsync(syntaxTree).ConfigureAwait(false);
|
||||
var referencedTypes = await getReferencedTypesAsync(semanticModel).ConfigureAwait(false);
|
||||
|
||||
referenceMap[TypeReference.FromSymbol(t.Symbol)] = referencedTypes.ToHashSet();
|
||||
|
||||
foreach (var referenced in referencedTypes)
|
||||
await buildReferenceMapRecursiveAsync(referenced).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (referenceMap.Count == 0)
|
||||
{
|
||||
// We have no cache available, so we must rebuild the whole map.
|
||||
await buildReferenceMapRecursiveAsync(TypeReference.FromSymbol(compiledTestType)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the reference map starting from a root type reference, connecting all types to their immediate references. Results are placed inside <see cref="referenceMap"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should not be used by itself. Use <see cref="buildReferenceMapAsync"/> instead.
|
||||
/// </remarks>
|
||||
/// <param name="rootReference">The root, where the map should start being build from.</param>
|
||||
private async Task buildReferenceMapRecursiveAsync(TypeReference rootReference)
|
||||
{
|
||||
var searchQueue = new ConcurrentBag<TypeReference> { rootReference };
|
||||
|
||||
while (searchQueue.Count > 0)
|
||||
{
|
||||
var toProcess = searchQueue.ToArray();
|
||||
searchQueue.Clear();
|
||||
|
||||
await Task.WhenAll(toProcess.Select(async toCheck =>
|
||||
{
|
||||
var referencedTypes = await getReferencedTypesAsync(toCheck).ConfigureAwait(false);
|
||||
referenceMap[toCheck] = referencedTypes;
|
||||
|
||||
foreach (var referenced in referencedTypes)
|
||||
{
|
||||
// We don't want to cycle over types that have already been explored.
|
||||
if (referenceMap.TryAdd(referenced, null))
|
||||
searchQueue.Add(referenced);
|
||||
}
|
||||
})).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all <see cref="TypeReference"/>s referenced by a given <see cref="TypeReference"/>, across all symbol sources.
|
||||
/// </summary>
|
||||
/// <param name="typeReference">The target <see cref="TypeReference"/>.</param>
|
||||
/// <returns>All <see cref="TypeReference"/>s referenced to across all symbol sources by <paramref name="typeReference"/>.</returns>
|
||||
private async Task<HashSet<TypeReference>> getReferencedTypesAsync(TypeReference typeReference)
|
||||
{
|
||||
var result = new HashSet<TypeReference>();
|
||||
|
||||
foreach (var reference in typeReference.Symbol.DeclaringSyntaxReferences)
|
||||
{
|
||||
var semanticModel = await getSemanticModelAsync(reference.SyntaxTree).ConfigureAwait(false);
|
||||
var referencedTypes = await getReferencedTypesAsync(semanticModel).ConfigureAwait(false);
|
||||
|
||||
foreach (var type in referencedTypes)
|
||||
result.Add(type);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all <see cref="TypeReference"/>s referenced by a given <see cref="SemanticModel"/>.
|
||||
/// </summary>
|
||||
/// <param name="semanticModel">The target <see cref="SemanticModel"/>.</param>
|
||||
/// <returns>All <see cref="TypeReference"/>s referenced by <paramref name="semanticModel"/>.</returns>
|
||||
private async Task<ICollection<TypeReference>> getReferencedTypesAsync(SemanticModel semanticModel)
|
||||
{
|
||||
var result = new ConcurrentDictionary<TypeReference, byte>();
|
||||
|
||||
var root = await semanticModel.SyntaxTree.GetRootAsync().ConfigureAwait(false);
|
||||
var descendantNodes = root.DescendantNodes(n =>
|
||||
{
|
||||
var kind = n.Kind();
|
||||
|
||||
// Ignored:
|
||||
// - Entire using lines.
|
||||
// - Namespace names (not entire namespaces).
|
||||
// - Entire static classes.
|
||||
// - Variable declarators (names of variables).
|
||||
// - The single IdentifierName child of an assignment expression (variable name), below.
|
||||
// - The single IdentifierName child of an argument syntax (variable name), below.
|
||||
// - The name of namespace declarations.
|
||||
// - Name-colon syntaxes.
|
||||
// - The expression of invocation expressions. Static classes are explicitly disallowed so the target type of an invocation must be available elsewhere in the syntax tree.
|
||||
// - The single IdentifierName child of a foreach expression (source variable name), below.
|
||||
// - The single 'var' IdentifierName child of a variable declaration, below.
|
||||
// - Element access expressions.
|
||||
|
||||
return kind != SyntaxKind.UsingDirective
|
||||
&& kind != SyntaxKind.NamespaceKeyword
|
||||
&& (kind != SyntaxKind.ClassDeclaration || ((ClassDeclarationSyntax)n).Modifiers.All(m => m.Kind() != SyntaxKind.StaticKeyword))
|
||||
&& (kind != SyntaxKind.QualifiedName || !(n.Parent is NamespaceDeclarationSyntax))
|
||||
&& kind != SyntaxKind.NameColon
|
||||
&& (kind != SyntaxKind.QualifiedName || n.Parent?.Kind() != SyntaxKind.NamespaceDeclaration)
|
||||
&& kind != SyntaxKind.ElementAccessExpression
|
||||
&& (n.Parent?.Kind() != SyntaxKind.InvocationExpression || n != ((InvocationExpressionSyntax)n.Parent).Expression);
|
||||
});
|
||||
|
||||
// This hashset is used to prevent re-exploring syntaxes with the same name.
|
||||
// Todo: This can be used across all files, but care needs to be taken for redefined types (via using X = y), using the same-named type from a different namespace, or via type hiding.
|
||||
var seenTypes = new ConcurrentDictionary<string, byte>();
|
||||
|
||||
await Task.WhenAll(descendantNodes.Select(node => Task.Run(() =>
|
||||
{
|
||||
if (node.Kind() == SyntaxKind.IdentifierName && node.Parent != null)
|
||||
{
|
||||
// Ignore the variable name of assignment expressions.
|
||||
if (node.Parent is AssignmentExpressionSyntax)
|
||||
return;
|
||||
|
||||
switch (node.Parent.Kind())
|
||||
{
|
||||
case SyntaxKind.VariableDeclarator: // Ignore the variable name of variable declarators.
|
||||
case SyntaxKind.Argument: // Ignore the variable name of arguments.
|
||||
case SyntaxKind.InvocationExpression: // Ignore a single identifier name expression of an invocation expression (e.g. IdentifierName()).
|
||||
case SyntaxKind.ForEachStatement: // Ignore a single identifier of a foreach statement (the source).
|
||||
case SyntaxKind.VariableDeclaration when node.ToString() == "var": // Ignore the single 'var' identifier of a variable declaration.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (node.Kind())
|
||||
{
|
||||
case SyntaxKind.GenericName:
|
||||
case SyntaxKind.IdentifierName:
|
||||
{
|
||||
string syntaxName = node.ToString();
|
||||
|
||||
if (seenTypes.ContainsKey(syntaxName))
|
||||
return;
|
||||
|
||||
if (!tryNode(node, out var symbol))
|
||||
return;
|
||||
|
||||
// The node has been processed so we want to avoid re-processing the same node again if possible, as this is a costly operation.
|
||||
// Note that the syntax name may differ from the finalised symbol name (e.g. member access).
|
||||
// We can only prevent future reprocessing if the symbol name and syntax name exactly match because we can't determine that the type won't be accessed later, such as:
|
||||
//
|
||||
// A.X = 5; // Syntax name = A, Symbol name = B
|
||||
// B.X = 5; // Syntax name = B, Symbol name = A
|
||||
// public A B;
|
||||
// public B A;
|
||||
//
|
||||
if (symbol.Name == syntaxName)
|
||||
seenTypes.TryAdd(symbol.Name, 0);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}))).ConfigureAwait(false);
|
||||
|
||||
return result.Keys;
|
||||
|
||||
bool tryNode(SyntaxNode node, out INamedTypeSymbol symbol)
|
||||
{
|
||||
if (semanticModel.GetSymbolInfo(node).Symbol is INamedTypeSymbol sType)
|
||||
{
|
||||
addTypeSymbol(sType);
|
||||
symbol = sType;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (semanticModel.GetTypeInfo(node).Type is INamedTypeSymbol tType)
|
||||
{
|
||||
addTypeSymbol(tType);
|
||||
symbol = tType;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Todo: Reduce the number of cases that fall through here.
|
||||
symbol = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
void addTypeSymbol(INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
var reference = TypeReference.FromSymbol(typeSymbol);
|
||||
|
||||
if (typeInheritsFromGame(reference))
|
||||
{
|
||||
logger.Add($"Type {typeSymbol.Name} inherits from game and is marked for exclusion.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Exclude types marked with the [ExcludeFromDynamicCompile] attribute
|
||||
// When multiple types exist in one file, the exclusion attribute may be omitted from some types, causing references to those types to indirectly compile explicitly excluded types.
|
||||
// If this type hasn't been seen before, do a manual pass over all its syntaxes to determine if an exclusion attribute is present anywhere in the file.
|
||||
if (!referenceMap.ContainsKey(reference))
|
||||
{
|
||||
foreach (var syntax in typeSymbol.DeclaringSyntaxReferences)
|
||||
{
|
||||
if (!syntaxExclusionMap.TryGetValue(syntax.SyntaxTree.FilePath, out bool containsExclusion))
|
||||
containsExclusion = syntaxExclusionMap[syntax.SyntaxTree.FilePath] = syntax.SyntaxTree.ToString().Contains(exclude_attribute_name);
|
||||
|
||||
if (containsExclusion)
|
||||
{
|
||||
logger.Add($"Type {typeSymbol.Name} referenced but marked for exclusion.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeSymbol.DeclaredAccessibility == Accessibility.Internal)
|
||||
assembliesContainingReferencedInternalMembers.TryAdd(typeSymbol.ContainingAssembly.Name, 0);
|
||||
|
||||
result.TryAdd(reference, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Traverses <see cref="referenceMap"/> to build a directed graph of <see cref="DirectedTypeNode"/> joined by their parents.
|
||||
/// </summary>
|
||||
/// <returns>A dictionary containing the directed graph from each <see cref="TypeReference"/> in <see cref="referenceMap"/>.</returns>
|
||||
private Dictionary<TypeReference, DirectedTypeNode> getDirectedGraph()
|
||||
{
|
||||
// Given the reference map (from above):
|
||||
//
|
||||
// P -> { C1, C2 }
|
||||
// C1 -> { C3, C4 }
|
||||
// C2 -> { C5, C6 }
|
||||
// C3 -> { }
|
||||
// C4 -> { C6 }
|
||||
// C5 -> { C6 }
|
||||
// C6 -> { C2 }
|
||||
//
|
||||
// The respective directed graph is built by traversing upwards and finding all incoming references at each type, such that:
|
||||
//
|
||||
// P -> { }
|
||||
// C1 -> { P }
|
||||
// C2 -> { C6, P, C5, C4, C2, C1 }
|
||||
// C3 -> { C1, P }
|
||||
// C4 -> { C1, P }
|
||||
// C5 -> { C2, P }
|
||||
// C6 -> { C5, C4, C2, C1, C6, P }
|
||||
//
|
||||
// The directed graph may contain cycles where multiple paths lead to the same node (e.g. C2, C6).
|
||||
|
||||
logger.Add("Retrieving reference graph...");
|
||||
|
||||
var result = new Dictionary<TypeReference, DirectedTypeNode>();
|
||||
|
||||
// Traverse through the reference map and assign parents to all children referenced types.
|
||||
foreach (var kvp in referenceMap)
|
||||
{
|
||||
var parentNode = getNode(kvp.Key);
|
||||
foreach (var typeRef in kvp.Value)
|
||||
getNode(typeRef).Parents.Add(parentNode);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
DirectedTypeNode getNode(TypeReference typeSymbol)
|
||||
{
|
||||
if (!result.TryGetValue(typeSymbol, out var existing))
|
||||
result[typeSymbol] = existing = new DirectedTypeNode(typeSymbol);
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Traverses a directed graph to find all direct and indirect references to a set of <see cref="TypeReference"/>s. References are returned as file names.
|
||||
/// </summary>
|
||||
/// <param name="sources">The <see cref="TypeReference"/>s to search from.</param>
|
||||
/// <param name="directedGraph">The directed graph generated through <see cref="getDirectedGraph"/>.</param>
|
||||
/// <returns>All files containing direct or indirect references to the given <paramref name="sources"/>.</returns>
|
||||
private HashSet<string> getReferencedFiles(IEnumerable<TypeReference> sources, IReadOnlyDictionary<TypeReference, DirectedTypeNode> directedGraph)
|
||||
{
|
||||
logger.Add("Retrieving referenced files...");
|
||||
|
||||
// Iterate through the graph and find the "expansion factor" at each node. The expansion factor is a count of how many nodes it or any of its parents have opened up.
|
||||
// As a node opens up more nodes, a successful re-compilation becomes increasingly improbable as integral parts of the game may start getting touched,
|
||||
// so the maximal expansion factor must be constrained to increase the probability of a successful re-compilation.
|
||||
foreach (var s in sources)
|
||||
computeExpansionFactors(directedGraph[s]);
|
||||
|
||||
// Invert the expansion factors such the changed file and the test will have the lowest values, and the centre of the graph will have the greatest values.
|
||||
ulong maxExpansionFactor = sources.Select(s => directedGraph[s].ExpansionFactor).Max();
|
||||
foreach (var (_, node) in directedGraph)
|
||||
node.ExpansionFactor = Math.Min(node.ExpansionFactor, maxExpansionFactor - node.ExpansionFactor);
|
||||
|
||||
var result = new HashSet<string>();
|
||||
foreach (var s in sources)
|
||||
getReferencedFilesRecursive(directedGraph[s], result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool computeExpansionFactors(DirectedTypeNode node, HashSet<DirectedTypeNode> seenTypes = null)
|
||||
{
|
||||
seenTypes ??= new HashSet<DirectedTypeNode>();
|
||||
if (seenTypes.Contains(node))
|
||||
return false;
|
||||
|
||||
seenTypes.Add(node);
|
||||
|
||||
node.ExpansionFactor = (ulong)node.Parents.Count;
|
||||
|
||||
foreach (var p in node.Parents)
|
||||
{
|
||||
if (computeExpansionFactors(p, seenTypes))
|
||||
node.ExpansionFactor += p.ExpansionFactor;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void getReferencedFilesRecursive(DirectedTypeNode node, HashSet<string> result, HashSet<DirectedTypeNode> seenTypes = null, int level = 0, SortedList<ulong> childExpansions = null)
|
||||
{
|
||||
// Don't go through duplicate nodes (multiple references from different types).
|
||||
seenTypes ??= new HashSet<DirectedTypeNode>();
|
||||
if (seenTypes.Contains(node))
|
||||
return;
|
||||
|
||||
seenTypes.Add(node);
|
||||
|
||||
// Concatenate the expansion factors from ourselves and the child.
|
||||
var expansions = new SortedList<ulong>();
|
||||
if (childExpansions != null)
|
||||
expansions.AddRange(childExpansions);
|
||||
expansions.AddRange(node.Parents.Where(p => p != node).Select(p => p.ExpansionFactor));
|
||||
|
||||
// Compute the "right bound" after which far outlier parents that expand too many nodes shouldn't be traversed.
|
||||
// This is calculated as 3x the inter-quartile range (see: https://en.wikipedia.org/wiki/Outlier#Tukey's_fences).
|
||||
double rightBound = double.PositiveInfinity;
|
||||
|
||||
if (expansions.Count > 1)
|
||||
{
|
||||
ulong q1 = getMedian(expansions.Take(expansions.Count / 2).ToList(), out int q1Centre);
|
||||
ulong q3 = getMedian(expansions.Skip((int)Math.Ceiling(expansions.Count / 2f)).ToList(), out _);
|
||||
|
||||
rightBound = q3 + 3 * (q3 - q1);
|
||||
|
||||
// Finally, remove all left-bound elements as they would skew the results as parents are traversed.
|
||||
expansions.RemoveRange(0, q1Centre);
|
||||
}
|
||||
|
||||
// Output the current iteration to the log. A '.' is prepended since the logger trims lines.
|
||||
logger.Add($"{(level > 0 ? $".{new string(' ', level * 2 - 1)}| " : string.Empty)} {node.ExpansionFactor} (rb: {rightBound}): {node}");
|
||||
|
||||
// Add all the current type's locations to the resulting set.
|
||||
foreach (var location in node.Reference.Symbol.Locations)
|
||||
{
|
||||
var syntaxTree = location.SourceTree;
|
||||
if (syntaxTree != null)
|
||||
result.Add(syntaxTree.FilePath);
|
||||
}
|
||||
|
||||
// Follow through the process for all parents.
|
||||
foreach (var p in node.Parents)
|
||||
{
|
||||
int nextLevel = level + 1;
|
||||
|
||||
// Right-bound outlier test - exclude parents greater than 3x IQR. Always expand left-bound parents as they are unlikely to cause compilation errors.
|
||||
if (p.ExpansionFactor > rightBound)
|
||||
{
|
||||
logger.Add($"{(nextLevel > 0 ? $".{new string(' ', nextLevel * 2 - 1)}| " : string.Empty)} {node.ExpansionFactor} (rb: {rightBound}): {node} (!! EXCLUDED !!)");
|
||||
continue;
|
||||
}
|
||||
|
||||
getReferencedFilesRecursive(p, result, seenTypes, nextLevel, expansions);
|
||||
}
|
||||
}
|
||||
|
||||
private ulong getMedian(List<ulong> range, out int centre)
|
||||
{
|
||||
centre = range.Count / 2;
|
||||
|
||||
// If count is odd - return the middle element.
|
||||
if (range.Count % 2 == 1)
|
||||
return range[centre];
|
||||
|
||||
// If count is even, return the average of the two nearest elements (centre is essentially the upper index).
|
||||
return (range[centre - 1] + range[centre]) / 2;
|
||||
}
|
||||
|
||||
private bool typeInheritsFromGame(TypeReference reference)
|
||||
{
|
||||
if (typeInheritsFromGameCache.TryGetValue(reference, out bool existing))
|
||||
return existing;
|
||||
|
||||
// When used via a nuget package, the local type name seems to always be more qualified than the symbol's type name.
|
||||
// E.g. Type name: osu.Framework.Game, symbol name: Framework.Game.
|
||||
if (typeof(Game).FullName?.Contains(reference.ToString()) == true)
|
||||
return typeInheritsFromGameCache[reference] = true;
|
||||
|
||||
if (reference.Symbol.BaseType == null)
|
||||
return typeInheritsFromGameCache[reference] = false;
|
||||
|
||||
return typeInheritsFromGameCache[reference] = typeInheritsFromGame(TypeReference.FromSymbol(reference.Symbol.BaseType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all the <see cref="TypeReference"/>s which list a given filename as any of their sources.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The target filename.</param>
|
||||
/// <returns>All <see cref="TypeReference"/>s with <paramref name="fileName"/> listed as one of their symbol locations.</returns>
|
||||
private IEnumerable<TypeReference> getTypesFromFile(string fileName) => referenceMap
|
||||
.Select(kvp => kvp.Key)
|
||||
.Where(t => t.Symbol.Locations.Any(l => l.SourceTree?.FilePath == fileName));
|
||||
|
||||
/// <summary>
|
||||
/// Compiles a <see cref="Project"/>.
|
||||
/// </summary>
|
||||
/// <param name="project">The <see cref="Project"/> to compile.</param>
|
||||
/// <returns>The resulting <see cref="Compilation"/>.</returns>
|
||||
private async Task<Compilation> compileProjectAsync(Project project)
|
||||
{
|
||||
if (compilationCache.TryGetValue(project, out var existing))
|
||||
return existing;
|
||||
|
||||
logger.Add($"Compiling project {project.Name}...");
|
||||
return compilationCache[project] = await project.GetCompilationAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="SemanticModel"/> from a given <see cref="SyntaxTree"/>.
|
||||
/// </summary>
|
||||
/// <param name="syntaxTree">The target <see cref="SyntaxTree"/>.</param>
|
||||
/// <returns>The corresponding <see cref="SemanticModel"/>.</returns>
|
||||
private async Task<SemanticModel> getSemanticModelAsync(SyntaxTree syntaxTree)
|
||||
{
|
||||
string filePath = syntaxTree.FilePath;
|
||||
|
||||
if (semanticModelCache.TryGetValue(filePath, out var existing))
|
||||
return existing;
|
||||
|
||||
var compilation = await compileProjectAsync(getProjectFromFile(filePath)).ConfigureAwait(false);
|
||||
|
||||
// Syntax trees are identified with the compilation they're in, so they must be re-retrieved on the new compilation.
|
||||
syntaxTree = compilation.SyntaxTrees.Single(t => t.FilePath == filePath);
|
||||
|
||||
return semanticModelCache[filePath] = compilation.GetSemanticModel(syntaxTree, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="Project"/> which contains a given filename as a document.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The target filename.</param>
|
||||
/// <returns>The <see cref="Project"/> that contains <paramref name="fileName"/>.</returns>
|
||||
private Project getProjectFromFile(string fileName) => solution.Projects.FirstOrDefault(p => p.Documents.Any(d => d.FilePath == fileName));
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the project which contains the currently-executing test.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="Project"/> containing the currently-executing test.</returns>
|
||||
private Project findTestProject()
|
||||
{
|
||||
string executingAssembly = Assembly.GetEntryAssembly()?.GetName().Name;
|
||||
return solution.Projects.FirstOrDefault(p => p.AssemblyName == executingAssembly);
|
||||
}
|
||||
|
||||
private void clearCaches()
|
||||
{
|
||||
compilationCache.Clear();
|
||||
semanticModelCache.Clear();
|
||||
syntaxExclusionMap.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a file in the solution with its new on-disk contents.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file to update.</param>
|
||||
private void updateFile(string fileName)
|
||||
{
|
||||
logger.Add($"Updating file {fileName} in solution...");
|
||||
|
||||
var changedDoc = solution.GetDocumentIdsWithFilePath(fileName)[0];
|
||||
solution = solution.WithDocumentText(changedDoc, SourceText.From(File.ReadAllText(fileName)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="INamedTypeSymbol"/> for stable inter-<see cref="Compilation"/> hashcode and equality comparisons.
|
||||
/// </summary>
|
||||
private readonly struct TypeReference : IEquatable<TypeReference>
|
||||
{
|
||||
public readonly INamedTypeSymbol Symbol;
|
||||
public readonly string ContainingNamespace;
|
||||
public readonly string SymbolName;
|
||||
|
||||
public TypeReference(INamedTypeSymbol symbol)
|
||||
{
|
||||
Symbol = symbol;
|
||||
ContainingNamespace = symbol.ContainingNamespace.ToString();
|
||||
SymbolName = symbol.ToString();
|
||||
}
|
||||
|
||||
public bool Equals(TypeReference other)
|
||||
=> ContainingNamespace == other.ContainingNamespace
|
||||
&& SymbolName == other.SymbolName;
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
hash.Add(SymbolName, StringComparer.Ordinal);
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
|
||||
public override string ToString() => SymbolName;
|
||||
|
||||
public static TypeReference FromSymbol(INamedTypeSymbol symbol) => new TypeReference(symbol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single node in the directed graph of <see cref="TypeReference"/>s, linked upwards by its parenting <see cref="DirectedTypeNode"/>.
|
||||
/// </summary>
|
||||
private class DirectedTypeNode : IEquatable<DirectedTypeNode>
|
||||
{
|
||||
public readonly TypeReference Reference;
|
||||
public readonly List<DirectedTypeNode> Parents = new List<DirectedTypeNode>();
|
||||
|
||||
/// <summary>
|
||||
/// The number of nodes expanded by this <see cref="DirectedTypeNode"/> and all parents recursively.
|
||||
/// </summary>
|
||||
public ulong ExpansionFactor;
|
||||
|
||||
public DirectedTypeNode(TypeReference reference)
|
||||
{
|
||||
Reference = reference;
|
||||
}
|
||||
|
||||
public bool Equals(DirectedTypeNode other)
|
||||
=> other != null
|
||||
&& Reference.Equals(other.Reference);
|
||||
|
||||
public override int GetHashCode() => Reference.GetHashCode();
|
||||
|
||||
public override string ToString() => Reference.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -249,9 +249,7 @@ namespace osu.Framework.Testing
|
||||
if (RuntimeInfo.IsDesktop)
|
||||
{
|
||||
backgroundCompiler = new DynamicClassCompiler<TestScene>();
|
||||
backgroundCompiler.CompilationStarted += compileStarted;
|
||||
backgroundCompiler.CompilationFinished += compileFinished;
|
||||
backgroundCompiler.CompilationFailed += compileFailed;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -275,18 +273,6 @@ namespace osu.Framework.Testing
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
backgroundCompiler?.Dispose();
|
||||
}
|
||||
|
||||
private void compileStarted() => Schedule(() =>
|
||||
{
|
||||
compilingNotice.Show();
|
||||
compilingNotice.FadeColour(Color4.White);
|
||||
});
|
||||
|
||||
private void compileFailed(Exception ex) => Schedule(() =>
|
||||
{
|
||||
Logger.Error(ex, "Error with dynamic compilation!");
|
||||
@@ -410,8 +396,6 @@ namespace osu.Framework.Testing
|
||||
CurrentTest.Dispose();
|
||||
}
|
||||
|
||||
var lastTest = CurrentTest;
|
||||
|
||||
CurrentTest = null;
|
||||
|
||||
if (testType == null && TestTypes.Count > 0)
|
||||
@@ -426,25 +410,6 @@ namespace osu.Framework.Testing
|
||||
|
||||
Debug.Assert(newTest != null);
|
||||
|
||||
const string dynamic_prefix = "dynamic";
|
||||
|
||||
// if we are a dynamically compiled type (via DynamicClassCompiler) we should update the dropdown accordingly.
|
||||
if (isDynamicLoad)
|
||||
{
|
||||
newTest.DynamicCompilationOriginal = lastTest?.DynamicCompilationOriginal ?? lastTest ?? newTest;
|
||||
toolbar.AddAssembly($"{dynamic_prefix} ({testType.Name})", testType.Assembly);
|
||||
}
|
||||
else
|
||||
{
|
||||
TestTypes.RemoveAll(t =>
|
||||
{
|
||||
Debug.Assert(t.Assembly.FullName != null);
|
||||
return t.Assembly.FullName.Contains(dynamic_prefix);
|
||||
});
|
||||
|
||||
newTest.DynamicCompilationOriginal = newTest;
|
||||
}
|
||||
|
||||
Assembly.Value = testType.Assembly;
|
||||
|
||||
CurrentTest = newTest;
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Framework.Testing
|
||||
{
|
||||
[ExcludeFromDynamicCompile]
|
||||
[TestFixture]
|
||||
public abstract class TestScene : Container, IDynamicallyCompile
|
||||
public abstract class TestScene : Container
|
||||
{
|
||||
public readonly FillFlowContainer<Drawable> StepsContainer;
|
||||
private readonly Container content;
|
||||
@@ -47,8 +47,6 @@ namespace osu.Framework.Testing
|
||||
/// </summary>
|
||||
private Game nestedGame;
|
||||
|
||||
public object DynamicCompilationOriginal { get; internal set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
{
|
||||
@@ -105,8 +103,6 @@ namespace osu.Framework.Testing
|
||||
|
||||
protected TestScene()
|
||||
{
|
||||
DynamicCompilationOriginal = this;
|
||||
|
||||
Name = RemovePrefix(GetType().ReadableName());
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@@ -52,11 +52,6 @@
|
||||
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.1221.0" />
|
||||
<!-- DO NOT use ProjectReference for native packaging project.
|
||||
See https://github.com/NuGet/Home/issues/4514 and https://github.com/dotnet/sdk/issues/765 . -->
|
||||
<PackageReference Include="Microsoft.Build.Locator" Version="1.4.1" />
|
||||
<PackageReference Include="OpenTabletDriver" Version="0.6.0.2" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.11.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="3.11.0">
|
||||
<NoWarn>NU1701</NoWarn> <!-- Requires .NETFramework for MSBuild, but we use Microsoft.Build.Locator which allows this package to work in .NETCoreApp. -->
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user