mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-03 11:58:24 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
504 lines
14 KiB
C#
504 lines
14 KiB
C#
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using System;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
|
|
namespace Sandbox.Generator;
|
|
|
|
public static class ILHotloadProcessor
|
|
{
|
|
internal static void Process( Processor processor )
|
|
{
|
|
processor.ILHotloadSupported = false;
|
|
|
|
if ( processor.LastSuccessfulCompilation == null || processor.BeforeILHotloadProcessingTrees.IsDefault )
|
|
{
|
|
return;
|
|
}
|
|
|
|
var oldTrees = GetSyntaxTreeDict( processor.BeforeILHotloadProcessingTrees );
|
|
var newTrees = GetSyntaxTreeDict( processor.Compilation.SyntaxTrees );
|
|
|
|
//
|
|
// Early out: check if files added / removed
|
|
//
|
|
|
|
if ( oldTrees.Count != newTrees.Count )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( oldTrees.Keys.Any( x => !newTrees.ContainsKey( x ) ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( newTrees.Keys.Any( x => !oldTrees.ContainsKey( x ) ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Compare each syntax tree.
|
|
// If only statement blocks have changed, inject attributes to mark the containing definitions.
|
|
//
|
|
|
|
var onlyBlocksChanged = true;
|
|
var updatedNewTrees = new ConcurrentBag<SyntaxTree>();
|
|
var exceptions = new ConcurrentBag<Exception>();
|
|
|
|
Parallel.ForEach( oldTrees.Values, oldTree =>
|
|
{
|
|
try
|
|
{
|
|
var newTree = newTrees[oldTree.FilePath];
|
|
|
|
if ( !HaveOnlyBlocksChanged( oldTree, newTree, out var updatedNewTree ) )
|
|
{
|
|
Volatile.Write( ref onlyBlocksChanged, false );
|
|
return;
|
|
}
|
|
|
|
updatedNewTrees.Add( updatedNewTree );
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
Volatile.Write( ref onlyBlocksChanged, false );
|
|
exceptions.Add( e );
|
|
}
|
|
} );
|
|
|
|
if ( exceptions.Count == 1 )
|
|
{
|
|
throw exceptions.First();
|
|
}
|
|
|
|
if ( exceptions.Count > 1 )
|
|
{
|
|
throw new AggregateException( exceptions );
|
|
}
|
|
|
|
//
|
|
// Abort if any tree had a more substantial change than just a statement block.
|
|
//
|
|
|
|
if ( !onlyBlocksChanged )
|
|
{
|
|
return;
|
|
}
|
|
|
|
processor.ILHotloadSupported = true;
|
|
|
|
//
|
|
// Add SupportsILHotloadAttribute
|
|
//
|
|
|
|
var oldAssemblyVersion = processor.LastSuccessfulCompilation.Assembly.Identity.Version;
|
|
|
|
var compilerExtraTree = processor.Compilation.SyntaxTrees
|
|
.Single( x => x.FilePath == Processor.CompilerExtraPath );
|
|
|
|
var compilerExtraText = compilerExtraTree.GetText();
|
|
|
|
var updatedCompilerExtraTree = compilerExtraTree.WithChangedText( compilerExtraText.WithChanges( new TextChange(
|
|
new TextSpan( compilerExtraText.Length, 0 ),
|
|
$"{Environment.NewLine}[assembly: global::Sandbox.SupportsILHotloadAttribute(\"{oldAssemblyVersion}\")]" ) ) );
|
|
|
|
processor.Compilation = processor.Compilation.ReplaceSyntaxTree( compilerExtraTree, updatedCompilerExtraTree );
|
|
|
|
//
|
|
// Apply changed syntax trees
|
|
//
|
|
|
|
foreach ( var updatedNewTree in updatedNewTrees )
|
|
{
|
|
var newTree = newTrees[updatedNewTree.FilePath];
|
|
|
|
processor.Compilation = processor.Compilation.ReplaceSyntaxTree( newTree, updatedNewTree );
|
|
}
|
|
}
|
|
|
|
private static Dictionary<string, SyntaxTree> GetSyntaxTreeDict( ImmutableArray<SyntaxTree> trees )
|
|
{
|
|
return trees
|
|
.Where( x => !x.FilePath.Equals( Processor.CompilerExtraPath, StringComparison.OrdinalIgnoreCase ) )
|
|
.ToDictionary( x => x.FilePath, x => x );
|
|
}
|
|
|
|
/// <summary>
|
|
/// For debugging, get a string representing the path to a node in an AST. Type and method names
|
|
/// are included.
|
|
/// </summary>
|
|
/// <param name="node">Node to find the path of.</param>
|
|
/// <returns>A string representing the path</returns>
|
|
private static string FormatNodePath( SyntaxNode node )
|
|
{
|
|
var thisNodeFormat = node.Kind().ToString();
|
|
|
|
switch ( node )
|
|
{
|
|
case MethodDeclarationSyntax methodDef:
|
|
thisNodeFormat = $"{thisNodeFormat}( {methodDef.Identifier.ValueText} )";
|
|
break;
|
|
case TypeDeclarationSyntax classDef:
|
|
thisNodeFormat = $"{thisNodeFormat}( {classDef.Identifier.ValueText} )";
|
|
break;
|
|
}
|
|
|
|
if ( node.Parent == null )
|
|
{
|
|
return thisNodeFormat;
|
|
}
|
|
|
|
return $"{FormatNodePath( node.Parent )} -> {thisNodeFormat}";
|
|
}
|
|
|
|
private static bool NodeIsDeclaration( SyntaxNode node )
|
|
{
|
|
// TODO: This might not be exhaustive!
|
|
|
|
switch ( node.Kind() )
|
|
{
|
|
case SyntaxKind.UsingDirective:
|
|
case SyntaxKind.ClassDeclaration:
|
|
case SyntaxKind.DelegateDeclaration:
|
|
case SyntaxKind.EnumDeclaration:
|
|
case SyntaxKind.StructDeclaration:
|
|
case SyntaxKind.RecordDeclaration:
|
|
case SyntaxKind.RecordStructDeclaration:
|
|
case SyntaxKind.MethodDeclaration:
|
|
case SyntaxKind.PropertyDeclaration:
|
|
case SyntaxKind.FieldDeclaration:
|
|
case SyntaxKind.EventDeclaration:
|
|
case SyntaxKind.ConstructorDeclaration:
|
|
case SyntaxKind.DestructorDeclaration:
|
|
case SyntaxKind.ConversionOperatorDeclaration:
|
|
case SyntaxKind.AddAccessorDeclaration:
|
|
case SyntaxKind.RemoveAccessorDeclaration:
|
|
case SyntaxKind.GetAccessorDeclaration:
|
|
case SyntaxKind.SetAccessorDeclaration:
|
|
case SyntaxKind.IndexerDeclaration:
|
|
case SyntaxKind.OperatorDeclaration:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test to see if the given node contains any declarations that would exclude it
|
|
/// from using <see cref="ILHotload"/>'s fast path.
|
|
/// </summary>
|
|
private static bool NodeContainsDeclarations( SyntaxNode node )
|
|
{
|
|
foreach ( var descendant in node.DescendantNodesAndSelf() )
|
|
{
|
|
if ( NodeIsDeclaration( descendant ) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool HaveOnlyBlocksChanged( SyntaxTree oldTree, SyntaxTree newTree, out SyntaxTree updatedNewTree )
|
|
{
|
|
if ( oldTree == null && newTree == null )
|
|
{
|
|
throw new ArgumentNullException( nameof( newTree ),
|
|
$"Expected at least one of ({nameof( oldTree )}, {nameof( newTree )}) to not be null" );
|
|
}
|
|
|
|
updatedNewTree = oldTree;
|
|
|
|
if ( oldTree == null || newTree == null )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var oldDefines = new HashSet<string>( oldTree.Options.PreprocessorSymbolNames );
|
|
|
|
if ( !oldDefines.SetEquals( newTree.Options.PreprocessorSymbolNames ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( newTree.IsEquivalentTo( oldTree ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var changes = newTree.GetChanges( oldTree );
|
|
|
|
if ( changes.Count == 0 )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( !oldTree.TryGetRoot( out var oldRoot ) || !newTree.TryGetRoot( out var newRoot ) )
|
|
{
|
|
// Maybe throw?
|
|
return false;
|
|
}
|
|
|
|
var newSpanStartOffset = 0;
|
|
var replacements = new Dictionary<SyntaxNode, SyntaxNode>();
|
|
|
|
foreach ( var change in changes )
|
|
{
|
|
// For each change:
|
|
// 1. Find the deepest node containing that change in both the old and new ASTs
|
|
// 2. If either node isn't within a method declaration, fail the test
|
|
// 3. If either node contains a type or member declaration, fail the test
|
|
// 4. We found a valid change, add it to the set if given
|
|
|
|
var newSpan = new TextSpan( change.Span.Start + newSpanStartOffset, change.NewText?.Length ?? 0 );
|
|
|
|
newSpanStartOffset += (change.NewText?.Length ?? 0) - change.Span.Length;
|
|
|
|
var oldNode = oldRoot.FindNode( change.Span, getInnermostNodeForTie: true );
|
|
var newNode = newRoot.FindNode( newSpan, getInnermostNodeForTie: true );
|
|
|
|
var oldIsTrivia = IsTrivialChange( oldNode, change.Span );
|
|
var newIsTrivia = IsTrivialChange( newNode, newSpan );
|
|
|
|
if ( newIsTrivia && oldIsTrivia && string.Equals( newNode.ToString(), oldNode.ToString(), StringComparison.Ordinal ) )
|
|
{
|
|
// Only trivia changed
|
|
continue;
|
|
}
|
|
|
|
var oldMethodBlock = oldNode.FirstAncestorOrSelf<SyntaxNode>( IsDeclaringBlock );
|
|
var newMethodBlock = newNode.FirstAncestorOrSelf<SyntaxNode>( IsDeclaringBlock );
|
|
|
|
if ( oldMethodBlock == null || newMethodBlock == null )
|
|
{
|
|
// Ignore SourceLocation attribute parameter changes
|
|
|
|
var sourceLocation = SyntaxFactory.ParseName( "Sandbox.Internal.SourceLocation" );
|
|
|
|
var oldAttrib = oldNode.FirstAncestorOrSelf<AttributeListSyntax>( x => IsSingleAttribute( x, sourceLocation ) );
|
|
var newAttrib = newNode.FirstAncestorOrSelf<AttributeListSyntax>( x => IsSingleAttribute( x, sourceLocation ) );
|
|
|
|
if ( oldAttrib == null || newAttrib == null )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ( NodeContainsDeclarations( oldNode ) || NodeContainsDeclarations( newNode ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( TryHandleMethodDecl( oldMethodBlock, newMethodBlock, replacements ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( TryHandlePropertyDecl( oldMethodBlock, newMethodBlock, replacements ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if ( replacements.Count > 0 )
|
|
{
|
|
var root = newTree.GetRoot();
|
|
updatedNewTree = newTree.WithRootAndOptions(
|
|
root.ReplaceNodes( replacements.Keys, ( a, b ) => replacements[a] ),
|
|
newTree.Options );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can we ignore the given change because it only involves whitespace / comments?
|
|
/// </summary>
|
|
private static bool IsTrivialChange( SyntaxNode changedNode, TextSpan changedSpan )
|
|
{
|
|
return !changedNode.Span.OverlapsWith( changedSpan );
|
|
}
|
|
|
|
private static bool IsSingleAttribute( AttributeListSyntax node, NameSyntax name )
|
|
{
|
|
return node.Attributes.Count == 1 && node.Attributes[0].Name.IsEquivalentTo( name );
|
|
}
|
|
|
|
private static bool IsDeclaringBlock( SyntaxNode node )
|
|
{
|
|
if ( node.IsKind( SyntaxKind.ArrowExpressionClause ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( !node.IsKind( SyntaxKind.Block ) && !node.IsKind( SyntaxKind.ArrowExpressionClause ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return node.Parent.IsKind( SyntaxKind.MethodDeclaration )
|
|
|| node.Parent.IsKind( SyntaxKind.GetAccessorDeclaration )
|
|
|| node.Parent.IsKind( SyntaxKind.SetAccessorDeclaration );
|
|
}
|
|
|
|
private static bool TryHandleMethodDecl( SyntaxNode oldBlock, SyntaxNode newBlock, Dictionary<SyntaxNode, SyntaxNode> replacements )
|
|
{
|
|
if ( oldBlock.Parent is not MethodDeclarationSyntax || newBlock.Parent is not MethodDeclarationSyntax newMethodDecl )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var replacingMethodDecl = newMethodDecl.WithAttributeLists(
|
|
newMethodDecl.AttributeLists.Add(
|
|
SyntaxFactory.AttributeList( SyntaxFactory.SingletonSeparatedList(
|
|
SyntaxFactory.Attribute( SyntaxFactory.QualifiedName(
|
|
SyntaxFactory.AliasQualifiedName(
|
|
SyntaxFactory.IdentifierName( SyntaxFactory.Token( SyntaxKind.GlobalKeyword ) ),
|
|
SyntaxFactory.IdentifierName( "Sandbox" ) ),
|
|
SyntaxFactory.IdentifierName( "MethodBodyChangeAttribute" )
|
|
) )
|
|
) )
|
|
)
|
|
);
|
|
|
|
replacements[newMethodDecl] = replacingMethodDecl;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryHandlePropertyDecl( SyntaxNode oldBlock, SyntaxNode newBlock, Dictionary<SyntaxNode, SyntaxNode> replacements )
|
|
{
|
|
if ( !TryGetPropertyDeclarationFromBlock( oldBlock, out _, out _ ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( !TryGetPropertyDeclarationFromBlock( newBlock, out var newPropertyDecl, out var accessor ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var replacingPropertyDecl = newPropertyDecl.WithAttributeLists(
|
|
newPropertyDecl.AttributeLists.Add(
|
|
SyntaxFactory.AttributeList( SyntaxFactory.SingletonSeparatedList(
|
|
SyntaxFactory.Attribute(
|
|
SyntaxFactory.QualifiedName( SyntaxFactory.AliasQualifiedName(
|
|
SyntaxFactory.IdentifierName( SyntaxFactory.Token( SyntaxKind.GlobalKeyword ) ),
|
|
SyntaxFactory.IdentifierName( "Sandbox" ) ),
|
|
SyntaxFactory.IdentifierName( "PropertyAccessorBodyChangeAttribute" )
|
|
),
|
|
SyntaxFactory.AttributeArgumentList( SyntaxFactory.SingletonSeparatedList(
|
|
SyntaxFactory.AttributeArgument( SyntaxFactory.MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.AliasQualifiedName(
|
|
SyntaxFactory.IdentifierName( SyntaxFactory.Token( SyntaxKind.GlobalKeyword ) ),
|
|
SyntaxFactory.IdentifierName( "Sandbox" ) ),
|
|
SyntaxFactory.IdentifierName( "PropertyAccessor" ) ),
|
|
SyntaxFactory.IdentifierName( accessor.ToString() ) ) ) ) )
|
|
)
|
|
) )
|
|
)
|
|
);
|
|
|
|
replacements[newPropertyDecl] = replacingPropertyDecl;
|
|
return true;
|
|
}
|
|
|
|
private enum PropertyAccessor
|
|
{
|
|
Get,
|
|
Set
|
|
}
|
|
|
|
private static bool TryGetPropertyDeclarationFromBlock( SyntaxNode block, out PropertyDeclarationSyntax outPropDecl, out PropertyAccessor accessor )
|
|
{
|
|
if ( block.Parent is PropertyDeclarationSyntax propDecl )
|
|
{
|
|
outPropDecl = propDecl;
|
|
accessor = PropertyAccessor.Get;
|
|
return true;
|
|
}
|
|
|
|
if ( block.Parent is AccessorDeclarationSyntax accDecl && accDecl.Parent is AccessorListSyntax accList )
|
|
{
|
|
outPropDecl = accList.Parent as PropertyDeclarationSyntax;
|
|
accessor = accDecl.Keyword.Text == "get" ? PropertyAccessor.Get : PropertyAccessor.Set;
|
|
return outPropDecl != null;
|
|
}
|
|
|
|
outPropDecl = null;
|
|
accessor = default;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compare lists of syntax trees, and log any changes. Includes the syntax node path to each change.
|
|
/// </summary>
|
|
/// <param name="oldTrees">Syntax trees from a previous compilation</param>
|
|
/// <param name="newTrees">New syntax trees to compare</param>
|
|
public static string GetChanges( SyntaxTree oldTree, SyntaxTree newTree )
|
|
{
|
|
var changes = newTree.GetChanges( oldTree );
|
|
|
|
if ( changes.Count == 0 )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( !oldTree.TryGetRoot( out var oldRoot ) )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var writer = new StringWriter();
|
|
|
|
foreach ( var change in changes )
|
|
{
|
|
var node = oldRoot.FindNode( change.Span, getInnermostNodeForTie: true );
|
|
var isTrivia = !node.Span.OverlapsWith( change.Span );
|
|
|
|
var startPosition = oldTree.GetLocation( change.Span ).GetLineSpan().StartLinePosition;
|
|
|
|
writer.WriteLine( $"{FormatNodePath( node )}{(isTrivia ? " (trivia)" : "")}" );
|
|
|
|
var oldLines = oldRoot.GetText().ToString( change.Span ).Split( '\n' );
|
|
var newLines = change.NewText?.Split( '\n' ) ?? Array.Empty<string>();
|
|
|
|
if ( oldLines.Length + newLines.Length > 5 )
|
|
{
|
|
writer.WriteLine( $"REMOVED {oldLines.Length}, ADDED {newLines.Length} LINES" );
|
|
continue;
|
|
}
|
|
|
|
var offset = 0;
|
|
foreach ( var line in oldLines )
|
|
{
|
|
writer.WriteLine( $"LINE {(startPosition.Line + ++offset).ToString().PadRight( 4 )} --- {line}" );
|
|
}
|
|
|
|
offset = 0;
|
|
foreach ( var line in newLines )
|
|
{
|
|
writer.WriteLine( $"LINE {(startPosition.Line + ++offset).ToString().PadRight( 4 )} +++ {line}" );
|
|
}
|
|
}
|
|
|
|
return writer.ToString();
|
|
}
|
|
}
|