Files
sbox-public/engine/Sandbox.Generator/Processor.cs
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

185 lines
5.4 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Sandbox.Generator;
/// <summary>
/// A sync object to allow workers to exchange data abstractly. Should lock Worker.Sync when accessing.
/// </summary>
internal class Sync
{
HashSet<string> tags = new HashSet<string>();
/// <summary>
/// Add a tag. Return true if added, return false if not. This is used to let other
/// workers know that we're adding some code, so they don't also try to add the same code.
/// ie - adding attributes to classes can fuck up if they have a partial spread over multiple
/// files, because we'll try to add the attribute for each file.
/// </summary>
public bool AddTag( string tag )
{
if ( tags.Contains( tag ) )
return false;
tags.Add( tag );
return true;
}
}
public class Processor
{
public static Func<string, string> DefaultPackageAssetResolver;
/// <summary>
/// Generated file that will get stuff like global usings and assembly attributes.
/// </summary>
public const string CompilerExtraPath = "/.obj/__compiler_extra.cs";
public string AddonName { get; set; } = "AddonName";
public Dictionary<string, string> AddonFileMap { get; set; } = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
public CSharpCompilation Compilation { get; set; }
public CSharpCompilation LastSuccessfulCompilation { get; set; }
public ImmutableArray<SyntaxTree> BeforeILHotloadProcessingTrees { get; set; }
public Exception Exception { get; internal set; }
public SourceProductionContext? Context { get; set; }
public List<Diagnostic> Diagnostics = new List<Diagnostic>();
public bool ILHotloadSupported { get; set; }
/// <summary>
/// A function that will take a package name and return the path to the asset
/// </summary>
public Func<string, string> PackageAssetResolver { get; set; } = DefaultPackageAssetResolver;
public void AddTrees( IEnumerable<SyntaxTree> trees )
{
if ( Context == null )
{
Compilation = Compilation.AddSyntaxTrees( trees.ToArray() );
}
foreach ( var tree in trees )
{
Context?.AddSource( tree.FilePath, SourceText.From( tree.ToString(), Encoding.UTF8 ) );
}
}
/// <summary>
/// Can be called manually
/// </summary>
public void Run( CSharpCompilation compilation,
CSharpCompilation lastSuccessfulCompilation = null,
ImmutableArray<SyntaxTree> lastBeforeIlHotloadProcessingTrees = default )
{
Compilation = compilation;
LastSuccessfulCompilation = lastSuccessfulCompilation;
BeforeILHotloadProcessingTrees = lastBeforeIlHotloadProcessingTrees;
try
{
if ( Compilation.SyntaxTrees.Count() > 0 )
{
var sync = new Sync();
ConcurrentBag<Worker> workers = new();
ConcurrentBag<Exception> exceptions = new();
//
// Run all the processers in tasks so it's super fast
//
var result = System.Threading.Tasks.Parallel.ForEach( Compilation.SyntaxTrees, tree =>
{
try
{
var w = Worker.Process( Compilation, tree, AddonFileMap, Context == null, sync, this );
workers.Add( w );
}
catch ( System.Exception e )
{
exceptions.Add( e );
}
} );
// any exceptions?
if ( exceptions.Any() )
{
throw exceptions.First();
}
//
// Sort the workers, so added code is in a deterministic order
//
var sortedWorkers = workers.OrderBy( x => x.TreeInput.FilePath ).ToArray();
//
// Process the results
//
foreach ( var worker in sortedWorkers )
{
// Don't need to do this if just using Source Generator
if ( Context == null )
{
Compilation = Compilation.ReplaceSyntaxTree( worker.TreeInput, CSharpSyntaxTree.Create( worker.OutputNode, worker.TreeInput.Options as CSharpParseOptions, worker.TreeInput.FilePath, worker.TreeInput.Encoding ) );
// Copy each worker's diagnostics so they're accessible outside of Sandbox.Generator
Diagnostics.AddRange( worker.Diagnostics );
}
else
{
foreach ( var diag in worker.Diagnostics )
{
Context.Value.ReportDiagnostic( diag );
}
}
}
//
// If trees were added, add them to the source
//
var trees = sortedWorkers.SelectMany( x => x.AddedTrees ).ToList();
//
// If we added loose code
//
var extraCode = string.Join( "\n", sortedWorkers.Where( x => !string.IsNullOrEmpty( x.AddedCode ) ).Select( x => x.AddedCode ) );
if ( !string.IsNullOrWhiteSpace( extraCode ) )
{
trees.Add( CSharpSyntaxTree.ParseText( extraCode, path: "_gen__AddedCode.cs", encoding: System.Text.Encoding.UTF8 ) );
}
//
// Write all the new trees
//
if ( trees.Count() > 0 )
{
AddTrees( trees );
}
}
var beforeIlHotloadTrees = Compilation.SyntaxTrees;
if ( Context == null )
{
ILHotloadProcessor.Process( this );
}
BeforeILHotloadProcessingTrees = beforeIlHotloadTrees;
}
catch ( System.Exception e )
{
Exception = e;
var desc = new DiagnosticDescriptor( "SB5000", "Generator Crash", $"Code Generator Crashed {Exception.StackTrace.Trim( '\n', '\r', ' ', '\t' ).Replace( "\n", "" ).Replace( "\r", "" )} - {e.Message}", "generator", DiagnosticSeverity.Error, true );
Context?.ReportDiagnostic( Diagnostic.Create( desc, null ) );
}
}
}