2025-02-04 19:02:39 +03:00
using System ;
2025-02-04 22:10:48 +03:00
using System.Collections.Generic ;
2025-02-04 19:02:39 +03:00
using System.Collections.Immutable ;
2025-02-04 22:10:48 +03:00
using System.ComponentModel ;
2025-02-04 19:02:39 +03:00
using System.Linq ;
using System.Text ;
using Microsoft.CodeAnalysis ;
using Microsoft.CodeAnalysis.CSharp.Syntax ;
using Microsoft.CodeAnalysis.Text ;
namespace mROA.Codegen ;
/// <summary>
/// A sample source generator that creates a custom report based on class properties. The target class should be annotated with the 'Generators.ReportAttribute' attribute.
/// When using the source code as a baseline, an incremental source generator is preferable because it reduces the performance overhead.
/// </summary>
[Generator]
public class SampleIncrementalSourceGenerator : IIncrementalGenerator
{
private const string Namespace = "mROA.Implementation" ;
private const string AttributeName = "SharedObjectInterafceAttribute" ;
private const string AttributeSourceCode = $@"// <auto-generated/>
namespace {Namespace}
{{
[System.AttributeUsage(System.AttributeTargets.Class)]
public class {AttributeName} : System.Attribute
{{
}}
}}" ;
public void Initialize ( IncrementalGeneratorInitializationContext context )
{
// Filter classes annotated with the [Report] attribute. Only filtered Syntax Nodes can trigger code generation.
var provider = context . SyntaxProvider
. CreateSyntaxProvider (
( s , _ ) => s is InterfaceDeclarationSyntax ,
( ctx , _ ) => GetClassDeclarationForSourceGen ( ctx ))
. Where ( t => t . reportAttributeFound )
. Select (( t , _ ) => t . Item1 );
// Generate the source code.
context . RegisterSourceOutput ( context . CompilationProvider . Combine ( provider . Collect ()),
(( ctx , t ) => GenerateCode ( ctx , t . Left , t . Right )));
}
/// <summary>
/// Checks whether the Node is annotated with the [Report] attribute and maps syntax context to the specific node type (ClassDeclarationSyntax).
/// </summary>
/// <param name="context">Syntax context, based on CreateSyntaxProvider predicate</param>
/// <returns>The specific cast and whether the attribute was found.</returns>
private static ( InterfaceDeclarationSyntax , bool reportAttributeFound ) GetClassDeclarationForSourceGen (
GeneratorSyntaxContext context )
{
var classDeclarationSyntax = ( InterfaceDeclarationSyntax ) context . Node ;
// Go through all attributes of the class.
foreach ( AttributeListSyntax attributeListSyntax in classDeclarationSyntax . AttributeLists )
foreach ( AttributeSyntax attributeSyntax in attributeListSyntax . Attributes )
{
if ( context . SemanticModel . GetSymbolInfo ( attributeSyntax ). Symbol is not IMethodSymbol attributeSymbol )
continue ; // if we can't get the symbol, ignore it
string attributeName = attributeSymbol . ContainingType . ToDisplayString ();
// Check the full name of the [Report] attribute.
if ( attributeName == "mROA.Implementation.SharedObjectInterfaceAttribute" )
return ( classDeclarationSyntax , true );
}
return ( classDeclarationSyntax , false );
}
/// <summary>
/// Generate code action.
/// It will be executed on specific nodes (ClassDeclarationSyntax annotated with the [Report] attribute) changed by the user.
/// </summary>
/// <param name="context">Source generation context used to add source files.</param>
/// <param name="compilation">Compilation used to provide access to the Semantic Model.</param>
/// <param name="classDeclarations">Nodes annotated with the [Report] attribute that trigger the generate action.</param>
private void GenerateCode ( SourceProductionContext context , Compilation compilation ,
ImmutableArray < InterfaceDeclarationSyntax > classDeclarations )
{
2025-02-04 22:10:48 +03:00
var methods = new List <( string , IMethodSymbol )>();
2025-02-05 00:26:28 +03:00
var frontendContextRepo = new List < string >();
2025-02-04 19:02:39 +03:00
// Go through all filtered class declarations.
foreach ( var classDeclarationSyntax in classDeclarations )
{
// We need to get semantic model of the class to retrieve metadata.
var semanticModel = compilation . GetSemanticModel ( classDeclarationSyntax . SyntaxTree );
2025-02-04 22:10:48 +03:00
2025-02-04 19:02:39 +03:00
// Symbols allow us to get the compile-time information.
if ( semanticModel . GetDeclaredSymbol ( classDeclarationSyntax ) is not INamedTypeSymbol classSymbol )
continue ;
2025-02-05 00:26:28 +03:00
2025-02-04 19:02:39 +03:00
var namespaceName = classSymbol . ContainingNamespace . ToDisplayString ();
// 'Identifier' means the token of the node. Get class name from the syntax node.
var className = classDeclarationSyntax . Identifier . Text ;
// Go through all class members with a particular type (property) to generate method lines.
var methodBody = classSymbol . GetMembers ()
. OfType < IMethodSymbol >();
2025-02-04 20:57:04 +03:00
var originalName = className ;
2025-02-04 19:02:39 +03:00
// Build up the source code
className = className . TrimStart ( 'I' ) + "RemoteEndpoint" ;
2025-02-04 23:19:24 +03:00
var methodsText = new List < string >();
foreach ( var method in methodBody )
{
var index = methods . Count ;
2025-02-05 10:58:50 +03:00
methods . Add (( namespaceName + "." + originalName , method ));
2025-02-04 23:19:24 +03:00
var sb = new StringBuilder ();
//Creating signature
// if (method.Parameters.Length == 0)
// sb.AppendLine($"public {method.ReturnType.ToDisplayString()} {method.Name}(){{");
// else
2025-02-04 23:30:52 +03:00
bool isAsync = method . ReturnType . Name == "Task" ;
2025-02-04 23:19:24 +03:00
sb . AppendLine ( "public" + ( isAsync
? " async "
: " " ) +
$"{method.ReturnType.ToDisplayString()} {method.Name}({string.Join(" , ", method.Parameters.Select(p => p.ToDisplayString()))}){{" );
//Creating request
2025-02-04 23:30:52 +03:00
if ( method . Parameters . Length == 1 && ! isAsync ||
method . Parameters . Length == 2 && isAsync )
2025-02-04 23:19:24 +03:00
{
sb . AppendLine (
$"\t\tvar request = new DefaultCallRequest {{ CommandId = {index}, ObjectId = id, Parameter = {method.Parameters.First().Name} }};" );
}
2025-02-04 23:30:52 +03:00
else
{
sb . AppendLine (
$"\t\tvar request = new DefaultCallRequest {{ CommandId = {index}, ObjectId = id }};" );
}
2025-02-04 23:19:24 +03:00
//Post created request
sb . AppendLine ( "\t\tserialisationModule.PostCallRequest(request);" );
if ( method . ReturnType . ToString () == "System.Threading.Tasks.Task" )
{
sb . AppendLine (
"\t\tawait serialisationModule.GetNextCommandExecution<FinalCommandExecution>(request.CallRequestId);" );
}
else if ( method . ReturnType . OriginalDefinition . ToString () == "System.Threading.Tasks.Task<TResult>" )
{
var type = method . ReturnType . ToString (). Split ([ '<' , '>' ], StringSplitOptions . RemoveEmptyEntries )
. Last ();
sb . AppendLine (
$"\t\tvar response = await serialisationModule.GetFinalCommandExecution<{type}>(request.CallRequestId);" );
sb . AppendLine ( $"\t\treturn ({type})response.Result;" );
}
else if (! isAsync && method . ReturnType . ToDisplayString () != "void" )
{
var type = method . ReturnType . ToDisplayString ();
sb . AppendLine (
$"\t\tvar response = serialisationModule.GetFinalCommandExecution<{type}>(request.CallRequestId).GetAwaiter().GetResult();" );
sb . AppendLine ( $"\t\treturn ({type})response.Result;" );
}
sb . AppendLine ( "\t}" );
methodsText . Add ( sb . ToString ());
}
2025-02-04 19:02:39 +03:00
var code = $@"// <auto-generated/>
using System;
2025-02-04 20:57:04 +03:00
using mROA.Implementation;
2025-02-04 19:02:39 +03:00
using System.Collections.Generic;
namespace {namespaceName};
2025-02-04 20:57:04 +03:00
partial class {className} (int id, ISerialisationModule.IFrontendSerialisationModule serialisationModule) : {originalName}, IRemoteObject
2025-02-04 19:02:39 +03:00
{{
public int Id => id;
2025-02-04 23:19:24 +03:00
{string.Join(" \ r \ n \ t ", methodsText)}
2025-02-04 19:02:39 +03:00
}}
";
2025-02-04 22:10:48 +03:00
2025-02-04 19:02:39 +03:00
// Add the source code to the compilation.
2025-02-04 23:19:24 +03:00
context . AddSource ( $"{className}.g.cs" , SourceText . From ( code , Encoding . UTF8 ));
2025-02-05 00:26:28 +03:00
frontendContextRepo . Add ( $"{{ typeof({classSymbol.ToDisplayString()}), typeof({namespaceName}.{className}) }}" );
2025-02-04 19:02:39 +03:00
}
2025-02-04 22:10:48 +03:00
var methodsStringed = methods . Select ( i =>
$"typeof({i.Item1}).GetMethod(\" { i . Item2 . Name } \ ", [{string.Join(" , ", i.Item2.Parameters.Select(p => $" typeof ({ p . Type . ToDisplayString ()}) "))}])" )
. ToList ();
var coCodegenRepoCode = @ $"// <auto-generated/>
using System.Collections.Generic;
using System.Reflection;
namespace mROA.Codegen;
public class CoCodegenMethodRepository :IMethodRepository
{{
private readonly List<MethodInfo> _methods = [
{string.Join(" , \ r \ n \ t \ t ", methodsStringed)}
];
public MethodInfo GetMethod ( int id )
{{
if ( _methods . Count <= id )
return null ;
return _methods [ id ];
}}
public int RegisterMethod ( MethodInfo method )
{{
_methods . Add ( method );
return _methods . Count - 1 ;
}}
public IEnumerable < MethodInfo > GetMethods ()
{{
return _methods ;
}}
2025-02-05 10:58:50 +03:00
public void Inject < T >( T dependency )
{{
}}
public void Bake ()
{{
}}
2025-02-04 22:10:48 +03:00
}}
";
context . AddSource ( $"CoCodegenMethodRepository.g.cs" , SourceText . From ( coCodegenRepoCode , Encoding . UTF8 ));
2025-02-05 00:26:28 +03:00
var fronendRepoCode = @ $"// <auto-generated/>
using System.Collections.Frozen;
2025-02-05 10:58:50 +03:00
namespace mROA.Codegen;
2025-02-05 00:26:28 +03:00
2025-02-05 10:58:50 +03:00
public class FrontendContextRepository : IContextRepository
2025-02-05 00:26:28 +03:00
{{
2025-02-05 10:58:50 +03:00
private ISerialisationModule.IFrontendSerialisationModule _serialisationModule;
2025-02-05 00:26:28 +03:00
private static readonly FrozenDictionary<Type, Type> _remoteTypes = new Dictionary<Type, Type> {{
{string.Join(" , \ r \ n \ t \ t ", frontendContextRepo)}}}.ToFrozenDictionary();
public int ResisterObject ( object o )
{{
throw new NotSupportedException ();
}}
public void ClearObject ( int id )
{{
throw new NotSupportedException ();
}}
public object GetObject ( int id )
{{
throw new NotSupportedException ();
}}
public T GetObject < T >( int id )
{{
if ( _remoteTypes . TryGetValue ( typeof ( T ), out var remoteType ))
{{
2025-02-05 10:58:50 +03:00
var remote = ( T ) Activator . CreateInstance ( remoteType , id , _serialisationModule )!;
2025-02-05 00:26:28 +03:00
return remote ;
}}
throw new NotSupportedException ();
}}
public object GetSingleObject ( Type type )
{{
2025-02-05 10:58:50 +03:00
return Activator . CreateInstance ( _remoteTypes [ type ], - 1 , _serialisationModule )!;
2025-02-05 00:26:28 +03:00
}}
public int GetObjectIndex ( object o )
{{
if ( o is IRemoteObject remote )
{{
return remote . Id ;
}}
throw new NotSupportedException ();
}}
2025-02-05 10:58:50 +03:00
public void Inject < T >( T dependency )
{{
if ( dependency is ISerialisationModule . IFrontendSerialisationModule serialisationModule )
_serialisationModule = serialisationModule ;
}}
public void Bake ()
{{
}}
2025-02-05 00:26:28 +03:00
}}
";
context . AddSource ( "FrontendContextRepository.g.cs" , SourceText . From ( fronendRepoCode , Encoding . UTF8 ));
2025-02-04 19:02:39 +03:00
}
}