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; /// /// A sync object to allow workers to exchange data abstractly. Should lock Worker.Sync when accessing. /// internal class Sync { HashSet tags = new HashSet(); /// /// 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. /// public bool AddTag( string tag ) { if ( tags.Contains( tag ) ) return false; tags.Add( tag ); return true; } } public class Processor { public static Func DefaultPackageAssetResolver; /// /// Generated file that will get stuff like global usings and assembly attributes. /// public const string CompilerExtraPath = "/.obj/__compiler_extra.cs"; public string AddonName { get; set; } = "AddonName"; public Dictionary AddonFileMap { get; set; } = new Dictionary( StringComparer.OrdinalIgnoreCase ); public CSharpCompilation Compilation { get; set; } public CSharpCompilation LastSuccessfulCompilation { get; set; } public ImmutableArray BeforeILHotloadProcessingTrees { get; set; } public Exception Exception { get; internal set; } public SourceProductionContext? Context { get; set; } public List Diagnostics = new List(); public bool ILHotloadSupported { get; set; } /// /// A function that will take a package name and return the path to the asset /// public Func PackageAssetResolver { get; set; } = DefaultPackageAssetResolver; public void AddTrees( IEnumerable trees ) { if ( Context == null ) { Compilation = Compilation.AddSyntaxTrees( trees.ToArray() ); } foreach ( var tree in trees ) { Context?.AddSource( tree.FilePath, SourceText.From( tree.ToString(), Encoding.UTF8 ) ); } } /// /// Can be called manually /// public void Run( CSharpCompilation compilation, CSharpCompilation lastSuccessfulCompilation = null, ImmutableArray lastBeforeIlHotloadProcessingTrees = default ) { Compilation = compilation; LastSuccessfulCompilation = lastSuccessfulCompilation; BeforeILHotloadProcessingTrees = lastBeforeIlHotloadProcessingTrees; try { if ( Compilation.SyntaxTrees.Count() > 0 ) { var sync = new Sync(); ConcurrentBag workers = new(); ConcurrentBag 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 ) ); } } }