Files
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

458 lines
14 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Sandbox.Utility;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Sandbox.Generator
{
internal sealed class Worker : CSharpSyntaxRewriter
{
/// <summary>
/// Create a new thread pool task to process this syntax tree
/// </summary>
internal static Worker Process( CSharpCompilation compilation, SyntaxTree tree, Dictionary<string, string> map, bool isInGame, Sync sync, Processor processor )
{
var m = new Worker( compilation, tree, map, isInGame, sync, processor );
m.Run();
return m;
}
/// <summary>
/// Don't access this willy nilly! It's not thread safe!
/// </summary>
public Processor Processor { get; set; }
/// <summary>
/// The compilation model
/// </summary>
CSharpCompilation Compilation;
/// <summary>
/// The original Syntax Tree
/// </summary>
public SyntaxTree TreeInput { get; private set; }
/// <summary>
/// True if we're doing an engine build, so should be doing more than adding syntax trees.
/// If it's false then we're probably intellisense or compiling externally
/// </summary>
public bool IsFullGeneration { get; private set; }
/// <summary>
/// Any syntax trees we added
/// </summary>
public List<SyntaxTree> AddedTrees { get; private set; } = new List<SyntaxTree>();
/// <summary>
/// Any loose code we want to add, but don't care where it ends up
/// </summary>
public string AddedCode { get; set; } = "";
/// <summary>
/// Semantic version of the original SyntaxTree
/// </summary>
internal SemanticModel Model;
/// <summary>
/// Allows workers to sync
/// </summary>
internal Sync Sync { get; private set; }
/// <summary>
/// The processed syntax tree
/// </summary>
public CSharpSyntaxNode OutputNode { get; private set; }
public Dictionary<string, string> AddonFileMap { get; }
internal Worker( CSharpCompilation compilation, SyntaxTree tree, Dictionary<string, string> map, bool isInGame, Sync sync, Processor processor )
{
Processor = processor;
Sync = sync;
IsFullGeneration = isInGame;
Compilation = compilation;
TreeInput = tree;
Model = Compilation.GetSemanticModel( tree );
AddonFileMap = map;
}
/// <summary>
/// Keep track of what classes we visited this run, so we don't end up putting duplicate DescriptionAttributes on partial classes
/// </summary>
internal static List<string> VisitedClasses = new List<string>();
/// <summary>
/// Runs in the thread pool, processes the syntax tree and returns
/// </summary>
internal void Run()
{
var node = TreeInput.GetRoot() as CSharpSyntaxNode;
OutputNode = Visit( node ) as CSharpSyntaxNode;
}
/// <summary>
/// Members to be added to the "current" class. Additions are added in a separate file, so
/// that intellisense will pick them up (they work with Source Generator)
/// </summary>
internal List<string> ClassAdditions = new List<string>();
/// <summary>
/// Members to be added to the current class, but not necessarily in an external file. Things that
/// don't need intellisensing use this, like backing fields.
/// </summary>
List<string> ClassModifiers = new List<string>();
/// <summary>
/// Attributes to be added to the current class.
/// </summary>
List<string> ClassBaseTypes = new List<string>();
public void AddToCurrentClass( string text, bool useSourceGen )
{
if ( useSourceGen ) ClassAdditions.Add( text );
else ClassModifiers.Add( text );
}
public void AddBaseTypeToCurrentClass( string text )
{
ClassBaseTypes.Add( text );
}
//
// Classblocks are things that aren't self contained, but have to exist in other blocks
// For example, when an RPC comes in we need to check which RPC it is. This is one of them.
//
internal List<ClassBlock> ClassBlocks = new List<ClassBlock>();
internal struct ClassBlock
{
public string Group;
public string Text;
public ITypeSymbol TypeSymbol;
}
internal void AddClassBlock( string group, string text, ITypeSymbol clss )
{
var b = new ClassBlock
{
Group = group,
Text = text,
TypeSymbol = clss
};
ClassBlocks.Add( b );
}
public override SyntaxNode VisitMethodDeclaration( MethodDeclarationSyntax _node )
{
var symbol = Model.GetDeclaredSymbol( _node );
ComponentSubscriberInterfaces.VisitMethod( _node, symbol, this );
var node = base.VisitMethodDeclaration( _node ) as MethodDeclarationSyntax;
Description.VisitMethod( ref node, symbol, this );
CodeGen.VisitMethod( ref node, symbol, this );
node = LinePreserve.AddLineNumber( node, _node, TreeInput, this );
node = ClassFileLocation.VisitNode( node, _node, symbol, this, TreeInput ) as MethodDeclarationSyntax;
return node;
}
bool IsGeneratedRazorFile()
{
return TreeInput.FilePath.StartsWith( "_gen_" ) && TreeInput.FilePath.Contains( ".razor_" );
}
public override SyntaxNode VisitExpressionStatement( ExpressionStatementSyntax _node )
{
var node = base.VisitExpressionStatement( _node ) as ExpressionStatementSyntax;
// Razor already does this
if ( IsGeneratedRazorFile() ) return node;
node = LinePreserve.AddLineNumber( node, _node, TreeInput, this );
return node;
}
public override SyntaxNode VisitAnonymousMethodExpression( AnonymousMethodExpressionSyntax _node )
{
var node = base.VisitAnonymousMethodExpression( _node ) as AnonymousMethodExpressionSyntax;
node = LinePreserve.AddLineNumber( node, _node, TreeInput, this );
return node;
}
public override SyntaxNode VisitInvocationExpression( InvocationExpressionSyntax node )
{
var location = node.GetLocation();
var symbolInfo = Model.GetSymbolInfo( node.Expression );
node = base.VisitInvocationExpression( node ) as InvocationExpressionSyntax;
var symlist = symbolInfo.CandidateSymbols;
if ( symbolInfo.Symbol is not null ) symlist = ImmutableArray.Create( symbolInfo.Symbol );
CloudAssetProvider.VisitInvocation( ref node, location, symlist, this );
StringTokenUpgrader.VisitInvocation( ref node, location, symlist, this );
return node;
}
public override SyntaxNode VisitBlock( BlockSyntax node )
{
node = base.VisitBlock( node ) as BlockSyntax;
if ( IsGeneratedRazorFile() ) return node;
bool changes = false;
var statements = node.Statements;
// only add these blocks when actually generating the code
if ( false && IsFullGeneration )
{
for ( int i = 0; i < statements.Count; i++ )
{
//
// Put EnsureSufficientExecutionStack before any method call
//
if ( statements[i] is ExpressionStatementSyntax exprStatement && exprStatement.Expression is InvocationExpressionSyntax )
{
changes = true;
statements = statements.Insert( i, SyntaxFactory.ParseStatement( "global::System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack();\r\n" ) );
i++;
}
}
}
if ( changes )
return node.WithStatements( statements );
return node;
}
public override SyntaxNode VisitFieldDeclaration( FieldDeclarationSyntax _node )
{
var symbol = Model.GetDeclaredSymbol( _node );
var node = base.VisitFieldDeclaration( _node ) as FieldDeclarationSyntax;
node = ClassFileLocation.VisitNode( node, _node, symbol, this, TreeInput ) as FieldDeclarationSyntax;
return node;
}
public override SyntaxNode VisitEnumMemberDeclaration( EnumMemberDeclarationSyntax _node )
{
var symbol = Model.GetDeclaredSymbol( _node );
var node = base.VisitEnumMemberDeclaration( _node ) as EnumMemberDeclarationSyntax;
Description.VisitEnumMember( ref node, symbol, this );
return node;
}
public override SyntaxNode VisitPropertyDeclaration( PropertyDeclarationSyntax _node )
{
var symbol = Model.GetDeclaredSymbol( _node );
var node = base.VisitPropertyDeclaration( _node ) as PropertyDeclarationSyntax;
DefaultValue.VisitProperty( ref node, symbol, this );
Description.VisitProperty( ref node, symbol, this );
CodeGen.VisitProperty( ref node, symbol, this );
node = LinePreserve.AddLineNumber( node, _node, TreeInput, this );
node = ClassFileLocation.VisitNode( node, _node, symbol, this, TreeInput ) as PropertyDeclarationSyntax;
return node;
}
public override SyntaxNode VisitClassDeclaration( ClassDeclarationSyntax _node )
{
var symbol = Model.GetDeclaredSymbol( _node ) as INamedTypeSymbol;
var oldClassAdditions = ClassAdditions;
var oldClassModifiers = ClassModifiers;
var oldClassAttributes = ClassBaseTypes;
ClassAdditions = new List<string>();
ClassModifiers = new List<string>();
ClassBaseTypes = new List<string>();
var node = _node;
try
{
node = base.VisitClassDeclaration( _node ) as ClassDeclarationSyntax;
Description.VisitClass( ref node, symbol, this );
node = ClassFileLocation.VisitNode( node, _node, symbol, this, TreeInput ) as ClassDeclarationSyntax;
//
// Create new Syntax Trees for the additions
//
if ( ClassAdditions.Count > 0 )
{
if ( !node.Modifiers.Any( m => m.IsKind( SyntaxKind.PartialKeyword ) ) )
{
AddError( node.GetLocation(), $"Please declare class '{symbol.Name}' as a partial so we can add codegen to it" );
return node;
}
var filename = $"{System.IO.Path.GetFileNameWithoutExtension( TreeInput.FilePath )}_{node.Identifier}.cs";
//
// The same file can have several of the same class, make sure we give it a unique filename for each generated class
// Otherwise the generator is gonna absolutely shit itself
//
if ( AddedTrees.Any( t => t.FilePath == filename ) )
{
filename = $"{System.IO.Path.GetFileNameWithoutExtension( TreeInput.FilePath )}_{node.Identifier}_{node.SpanStart}.cs";
}
var code = new CodeWriter();
AddNamespaces( code, symbol );
code.WriteLine( "" );
code.StartClass( symbol );
foreach ( var add in ClassAdditions )
{
code.WriteLines( add );
}
code.EndClass( symbol );
var st = SyntaxFactory.ParseSyntaxTree( code.ToString(), TreeInput.Options, filename, TreeInput.Encoding );
AddedTrees.Add( st );
}
}
catch ( System.Exception e )
{
Console.WriteLine( e );
}
finally
{
//
// Add to this class node
//
if ( ClassModifiers.Count > 0 )
{
var members = ClassModifiers.Select( x => SyntaxFactory.ParseMemberDeclaration( x ) ).ToArray();
node = node.AddMembers( members );
}
if ( ClassBaseTypes.Count > 0 )
{
var baseTypes = ClassBaseTypes.Select( x => SyntaxFactory.SimpleBaseType( SyntaxFactory.ParseTypeName( x ) ) ).ToArray();
// Console.WriteLine( $"{symbol.Name} - {string.Join( ";", baseTypes.Select( x => x.ToString() ) )}" );
node = node.AddBaseListTypes( baseTypes );
}
ClassAdditions = oldClassAdditions;
ClassModifiers = oldClassModifiers;
ClassBaseTypes = oldClassAttributes;
}
node = LinePreserve.AddLineNumber( node, _node, TreeInput, this );
return node;
}
private void AddNamespaces( CodeWriter code, INamedTypeSymbol symbol )
{
//
// Collect usings
//
SyntaxList<UsingDirectiveSyntax> allUsings = SyntaxFactory.List<UsingDirectiveSyntax>();
foreach ( var syntaxRef in symbol.DeclaringSyntaxReferences )
{
foreach ( var parent in syntaxRef.GetSyntax().Ancestors( false ) )
{
if ( parent is NamespaceDeclarationSyntax nsParent )
allUsings = allUsings.AddRange( nsParent.Usings );
else if ( parent is CompilationUnitSyntax cuParent )
allUsings = allUsings.AddRange( cuParent.Usings );
}
}
// shitty DistinctBy - no .net6
SyntaxList<UsingDirectiveSyntax> distinctUsings = SyntaxFactory.List<UsingDirectiveSyntax>();
foreach ( var u in allUsings )
{
// skip global using, since they're already added
if ( !u.GlobalKeyword.IsKind( SyntaxKind.None ) )
continue;
if ( distinctUsings.Any( x => x.Name.ToString() == u.Name.ToString() ) )
continue;
distinctUsings = distinctUsings.Add( u );
}
//
// Add these if we don't already have them
//
if ( !distinctUsings.Any( x => x.Name.ToString() == "Sandbox" ) )
code.WriteLine( "using Sandbox;" );
if ( !distinctUsings.Any( x => x.Name.ToString() == "System.Collections.Generic" ) )
code.WriteLine( "using System.Collections.Generic;" );
//
// Replicate the same usings - means we don't have to massively overcomplicate initializer fields
//
code.WriteLines( distinctUsings.ToFullString() );
}
public List<Diagnostic> Diagnostics { get; } = new List<Diagnostic>();
internal void AddError( Location location, DiagnosticDescriptor diagnostic, params object[] messageArgs )
{
Diagnostics.Add( Diagnostic.Create( diagnostic, location, messageArgs ) );
}
internal void AddError( Location location, string error )
{
AddError( location, new DiagnosticDescriptor( "SB2000", "Net Not Supported", error, "generator", DiagnosticSeverity.Error, true ) );
}
internal void Log( string v, Location location = null )
{
v = v.Replace( "\n", "" );
v = v.Replace( "\r", "" );
var d = new DiagnosticDescriptor( "SB0002", "Net Not Supported", v, "generator", DiagnosticSeverity.Warning, true );
Diagnostics.Add( Diagnostic.Create( d, location ) );
}
internal Dictionary<string, INamedTypeSymbol> Types = new Dictionary<string, INamedTypeSymbol>();
public INamedTypeSymbol GetOrCreateTypeByMetadataName( string name )
{
if ( Types.TryGetValue( name, out INamedTypeSymbol type ) )
return type;
type = Compilation.GetTypeByMetadataName( name );
Types[name] = type;
return type;
}
}
}