// #define DONT_ADD
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using mROA.Codegen.Templates;
namespace mROA.Codegen
{
///
/// 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.
///
[Generator]
public class mROAGenerator : IIncrementalGenerator
{
private const string SharedObjectInterfaceAttributeName = "SharedObjectInterface";
private static readonly Predicate ParameterFilter =
i => i.Type.Name is "CancellationToken" or "RequestContext";
private static readonly Predicate ParameterFilterForType =
i => i.Name is "CancellationToken" or "RequestContext";
private int _currentInternalCallIndex;
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var syntaxes = context.SyntaxProvider.CreateSyntaxProvider(
NodeIsInterfaceWithSharedObjectInterfaceAttribute,
TransformToInterfaceDeclarationSyntax);
var incrementalValueProvider = context.CompilationProvider.Combine(syntaxes.Collect());
context.RegisterSourceOutput(incrementalValueProvider,
(productionContext, pair) => GenerateCode(productionContext, pair.Left, pair.Right));
}
private static InterfaceDeclarationSyntax TransformToInterfaceDeclarationSyntax(GeneratorSyntaxContext context,
CancellationToken _)
{
if (context.Node is not InterfaceDeclarationSyntax interfaceSyntax)
throw new InvalidOperationException();
return interfaceSyntax;
}
private static bool NodeIsInterfaceWithSharedObjectInterfaceAttribute(SyntaxNode node, CancellationToken _)
{
if (node is not InterfaceDeclarationSyntax interfaceSyntax)
return false;
var attributes = interfaceSyntax.AttributeLists.SelectMany(list => list.Attributes);
return attributes.Any(attribute => attribute.ToFullString() == SharedObjectInterfaceAttributeName);
}
private void GenerateCode(SourceProductionContext context, Compilation compilation,
ImmutableArray classes)
{
var methodRepoTemplate = new MethodRepoTemplate();
var typeBinder = new RemoteTypeBinderTemplate();
var indexProviderTemplate = new IndexProviderTemplate();
var totalMethods = new List();
if (compilation.AssemblyName == null)
throw new InvalidOperationException();
indexProviderTemplate.DefineNamespace(compilation.AssemblyName);
var invokers = new List();
var declarations = classes.ToList();
var apiLevel = 0;
foreach (var classDeclarationSyntax in declarations)
{
_currentInternalCallIndex = 0;
var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol)
continue;
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var className = classDeclarationSyntax.Identifier.Text;
var innerMethods = CodegenUtilities.CollectMembers(classSymbol);
var associated = innerMethods.Select(i => i.AssociatedSymbol).Where(i => i != null)
.Distinct(SymbolEqualityComparer.Default).Cast().ToList();
innerMethods = innerMethods.Where(i =>
i.MethodKind != MethodKind.EventAdd && i.MethodKind != MethodKind.EventRemove)
.ToList();
totalMethods.AddRange(innerMethods);
var originalName = className;
className = className.TrimStart('I') + "Proxy";
var proxyTemplate = new ProxyTemplate();
var propertiesAccessMethods = new List<(string, IMethodSymbol)>();
int startInvokers = invokers.Count;
foreach (var method in innerMethods)
switch (method.MethodKind)
{
case MethodKind.PropertyGet or MethodKind.PropertySet:
GeneratePropertyMethod(methodRepoTemplate, method, propertiesAccessMethods, invokers,
classSymbol);
continue;
default:
GenerateDeclaredMethod(proxyTemplate, methodRepoTemplate, method, invokers, classSymbol);
break;
}
foreach (var symbol in associated)
switch (symbol)
{
case IPropertySymbol propertySymbol:
var setter = propertiesAccessMethods.FirstOrDefault(i =>
i.Item2.AssociatedSymbol!.Name == propertySymbol.Name && i.Item2.ReturnsVoid);
var getter = propertiesAccessMethods.FirstOrDefault(i =>
i.Item2.AssociatedSymbol!.Name == propertySymbol.Name && !i.Item2.ReturnsVoid);
string impl;
if (propertySymbol.IsIndexer)
impl =
$"public {propertySymbol.Type.ToUnityString()} this[{string.Join(", ", propertySymbol.Parameters.Select(p => p.ToUnityString()))}] {{ {getter.Item1} {setter.Item1} }}";
else
impl =
$"public {propertySymbol.Type.ToUnityString()} {symbol.Name} {{ {getter.Item1} {setter.Item1} }}";
proxyTemplate.InsertMethods(impl);
// declaredMethods.Add(impl);
break;
case IEventSymbol eventSymbol:
proxyTemplate.InsertMethods($"public event {eventSymbol.Type.ToUnityString()}? {eventSymbol.Name};");
// declaredMethods.Add(
// $"public event {eventSymbol.Type.ToDisplayString()}? {eventSymbol.Name};");
break;
}
GenerateEventImplementation(proxyTemplate, methodRepoTemplate, typeBinder, classSymbol, invokers, context);
var endInvokers = invokers.Count;
indexProviderTemplate.InsertIndexSpan($"{{ typeof({originalName}), new[] {{ {CodegenUtilities.JoinWithComa(Enumerable.Range(startInvokers, endInvokers - startInvokers).Select(i => i.ToString()))} }} }},");
proxyTemplate.DefineClassName(className);
proxyTemplate.DefineOriginalName(originalName);
proxyTemplate.DefineNamespaceName(namespaceName);
var code = proxyTemplate.Compile();
// Add the source code to the compilation.
#if !DONT_ADD
context.AddSource($"{className}.g.cs", SourceText.From(code, Encoding.UTF8));
#endif
indexProviderTemplate.InsertRemoteTypePair($"{{ typeof({classSymbol.ToUnityString()}), (id, r, c, indices) => new {namespaceName}.{className}(id, r, c, indices) }}");
}
if (totalMethods.Count != 0)
{
var coCodegenRepoCode = methodRepoTemplate.Compile();
indexProviderTemplate.DefineLevel(apiLevel.ToString());
indexProviderTemplate.DefineLen(invokers.Count.ToString());
var providerCode = indexProviderTemplate.Compile();
#if !DONT_ADD
context.AddSource("GeneratedInvokersCollection.g.cs",
SourceText.From(coCodegenRepoCode, Encoding.UTF8));
context.AddSource("GeneratedIndexProvider.g.cs", SourceText.From(providerCode, Encoding.UTF8));
#endif
}
if (indexProviderTemplate.IsRemoteTypePairInserted())
{
var frontendRepoCode = typeBinder.Compile();
#if !DONT_ADD
context.AddSource("RemoteTypeBinder.g.cs", SourceText.From(frontendRepoCode, Encoding.UTF8));
#endif
}
}
private void GenerateEventImplementation(ProxyTemplate proxyTemplate, MethodRepoTemplate methodRepoTemplate, RemoteTypeBinderTemplate remoteTypeBinder, INamedTypeSymbol classSymbol,
List invokers, SourceProductionContext context)
{
var events = classSymbol.AllInterfaces.Add(classSymbol).SelectMany(i => i.GetMembers())
.OfType().ToList();
if (events.Count == 0)
return;
var partialInterfaceTemplate = new PartialInterfaceTemplate();
partialInterfaceTemplate.DefineName(classSymbol.Name);
partialInterfaceTemplate.DefineNamespace(classSymbol.ContainingNamespace.ToDisplayString());
var objectBinderTemplate = remoteTypeBinder.CloneInnerObjectBinder();
foreach (var currentEvent in events)
{
var additionalMethod = GenerateMethodExternalCaller(currentEvent, out var signature);
proxyTemplate.InsertMethods(additionalMethod);
// declaredMethods.Add(additionalMethod);
partialInterfaceTemplate.InsertSignature(signature);
GenerateEventCode(methodRepoTemplate, currentEvent, invokers, classSymbol);
GenerateBinderCode(objectBinderTemplate, currentEvent, classSymbol);
}
objectBinderTemplate.DefineType(classSymbol.ToUnityString());
var partialInterface = partialInterfaceTemplate.Compile();
var binder = objectBinderTemplate.Compile();
remoteTypeBinder.InsertEventBinder(binder);
#if !DONT_ADD
context.AddSource($"{classSymbol.Name}.g.cs", SourceText.From(partialInterface, Encoding.UTF8));
#endif
}
private static string GenerateMethodExternalCaller(IEventSymbol eventSymbol, out string interfaceSignature)
{
const string level = "\t\t";
var parameters = ((INamedTypeSymbol)eventSymbol.Type).TypeArguments;
var parameterIndex = 0;
var parametersDeclaration =
string.Join(", ", parameters.Select(i => $"{i.ToUnityString()} p{parameterIndex++}"));
var signature = $"public void {CodegenUtilities.EventExternalName(eventSymbol)}({parametersDeclaration})";
interfaceSignature = signature + ";";
var caller = $@"{signature}
{level}{{
{level} {eventSymbol.Name}?.Invoke({string.Join(", ", Enumerable.Range(0, parameterIndex).Select(i => "p" + i))});
{level}}}
";
return caller;
}
private void GenerateDeclaredMethod(ProxyTemplate proxyTemplate, MethodRepoTemplate methodRepoTemplate, IMethodSymbol method, List invokers,
INamedTypeSymbol baseInterface)
{
var sb = new StringBuilder();
bool isParametrized;
bool isAsync;
bool isVoid;
List? parameters;
switch (method.ReturnType)
{
case INamedTypeSymbol namedType:
isAsync = namedType.Name == "Task";
isVoid = (isAsync && namedType.TypeParameters.Length == 0) || namedType.Name == "Void";
parameters = method.Parameters.ToList();
parameters.RemoveAll(ParameterFilter);
isParametrized = parameters.Count != 0;
break;
case IArrayTypeSymbol:
isAsync = false;
isVoid = false;
parameters = new List();
isParametrized = method.Parameters.Length != 0;
break;
default:
return;
}
//Creating signature
sb.AppendLine("public" + (isAsync
? " async "
: " ") +
$"{method.ReturnType.ToUnityString()} {method.Name}({string.Join(", ", method.Parameters.Select(CodegenUtilities.ToFullString))}){{");
var isUntrusted = method.GetAttributes().Any(i => i.AttributeClass?.Name == "UntrustedAttribute");
var prefix = isAsync ? "await " : "";
var postfix = !isAsync ? isVoid ? ".Wait()" : ".GetAwaiter().GetResult()" : "";
var parameterLink = isParametrized
? ", new System.Object[] { " + string.Join(", ", parameters.Select(i => i.Name)) + " }"
: string.Empty;
string caller;
if (isUntrusted)
{
caller = $"CallUntrustedAsync(CallIndices[{_currentInternalCallIndex++}]{parameterLink})";
}
else
{
var tokenInsert = isAsync &&
method.Parameters.FirstOrDefault(i => i.Type.Name == "CancellationToken") is
{ } tokenSymbol
? ", cancellationToken : " + tokenSymbol.Name
: string.Empty;
caller = isVoid
? $"CallAsync(CallIndices[{_currentInternalCallIndex++}]{parameterLink}{tokenInsert})"
: isAsync
? $"GetResultAsync<{CodegenUtilities.ExtractTaskType(method.ReturnType)}>(CallIndices[{_currentInternalCallIndex++}]{parameterLink}{tokenInsert})"
: $"GetResultAsync<{CodegenUtilities.ToFullString(method.ReturnType)}>(CallIndices[{_currentInternalCallIndex++}]{parameterLink}{tokenInsert})";
if (!isVoid)
prefix = "return " + prefix;
}
sb.AppendLine("\t\t\t" + prefix + caller + postfix + ";");
sb.AppendLine("\t\t}");
proxyTemplate.InsertMethods(sb.ToString());
// declaredMethods.Add(sb.ToString());
var parameterTypes = string.Join(", ",
$"{string.Join(", ", parameters.Select(p => $"typeof({p.Type.ToUnityString()})"))}");
var level = "\t\t\t";
var parametersInsertList = new List();
var useCancellationToken = false;
foreach (var parameter in method.Parameters)
switch (parameter.Type.Name)
{
case "CancellationToken":
parametersInsertList.Add("(CancellationToken)special[1]");
useCancellationToken = true;
break;
case "RequestContext":
parametersInsertList.Add("(RequestContext)special[0]");
break;
default:
parametersInsertList.Add(CodegenUtilities.Caster(parameter.Type,
$"parameters[{parameters.IndexOf(parameter)}]"));
break;
}
var parametersInsert = string.Join(", ", parametersInsertList);
string backend;
string funcInvoking;
if (isAsync && !isVoid)
funcInvoking =
$"(i as {method.ContainingType.ToUnityString()}).{method.Name}({parametersInsert}).ContinueWith(t => {{ post(t.Result); }})";
else if (isAsync && isVoid)
funcInvoking =
$"(i as {method.ContainingType.ToUnityString()}).{method.Name}({parametersInsert}).ContinueWith(t => {{ post(null); }})";
else if (!isAsync && isVoid)
funcInvoking = $@"{{
{level} (i as {method.ContainingType.ToUnityString()}).{method.Name}({parametersInsert});
{level} return null;
{level} }}";
else
funcInvoking = $"(i as {method.ContainingType.ToUnityString()}).{method.Name}({parametersInsert})";
if (isAsync)
{
var invokerTemplate = methodRepoTemplate.CloneInnerAsyncInvoker();
invokerTemplate.DefineIsVoid(isVoid.ToString().ToLower());
invokerTemplate.DefineReturnType(CodegenUtilities.ExtractTaskType(method.ReturnType));
invokerTemplate.DefineParametersType(parameterTypes);
invokerTemplate.DefineSuitableType(baseInterface.ToUnityString());
invokerTemplate.DefineFuncInvoking(funcInvoking);
invokerTemplate.DefineIsTrusted((!isUntrusted).ToString().ToLower());
invokerTemplate.DefineCancellation(useCancellationToken.ToString().ToLower());
backend = invokerTemplate.Compile();
}
else
{
var invokerTemplate = methodRepoTemplate.CloneInnerSyncInvoker();
invokerTemplate.DefineIsVoid(isVoid.ToString().ToLower());
invokerTemplate.DefineReturnType(isVoid ? "void" : method.ReturnType.ToUnityString());
invokerTemplate.DefineParametersType(parameterTypes);
invokerTemplate.DefineSuitableType(baseInterface.ToUnityString());
invokerTemplate.DefineFuncInvoking(funcInvoking);
invokerTemplate.DefineIsTrusted((!isUntrusted).ToString().ToLower());
backend = invokerTemplate.Compile();
}
methodRepoTemplate.InsertInvoke(backend);
invokers.Add(backend);
}
private void GenerateBinderCode(ObjectBinderTemplate objectBinderTemplate, IEventSymbol eventSymbol, INamedTypeSymbol baseType)
{
var eventBinderTemplate = objectBinderTemplate.CloneInnerEventBinder();
var index = _currentInternalCallIndex;
var parameters = (eventSymbol.Type as INamedTypeSymbol)!.TypeArguments.ToList();
var parametersDeclaration = string.Join(", ",
CodegenUtilities.JoinWithComa(Enumerable.Range(0, parameters.Count).Select(i => "p" + i)));
var pi = 0;
var transferParameters = CodegenUtilities.JoinWithComa(parameters.Select(i => (i, pi++))
.Where(i => !ParameterFilterForType(i.i))
.Select(i => "p" + i.Item2));
var requestIndex = parameters.FindIndex(i => i.Name == "RequestContext");
if (requestIndex != -1)
{
var callFilter = $"\n\r\t\t\tif(ownerId == p{requestIndex}.OwnerId) return;";
eventBinderTemplate.DefineCallFilter(callFilter);
}
eventBinderTemplate.DefineType(baseType.ToUnityString());
eventBinderTemplate.DefineEventName(eventSymbol.Name);
eventBinderTemplate.DefineParametersDeclaration(parametersDeclaration);
eventBinderTemplate.DefineCommandIdTag($"context.CallIndexProvider.GetIndices(typeof({baseType.ToUnityString()}))[{index}]");
eventBinderTemplate.DefineTransferParameters(transferParameters);
var eventBinderCode = eventBinderTemplate.Compile();
objectBinderTemplate.InsertEventBinder(eventBinderCode);
}
private void GenerateEventCode(MethodRepoTemplate methodRepoTemplate, IEventSymbol eventSymbol, List invokers, ITypeSymbol baseInterface)
{
var level = "\t\t\t";
int pi = 0;
var parameters = ((INamedTypeSymbol)eventSymbol.Type).TypeArguments.Select(i => (i, pi++))
.ToImmutableArray();
var parsingParameters = parameters.RemoveAll(i => ParameterFilterForType(i.i)).ToList();
var parameterTypes = string.Join(", ",
$"{string.Join(", ", parsingParameters.Select(p => $"typeof({p.i.ToUnityString()})"))}");
var parametersInsertList = new List();
foreach (var parameter in parameters)
switch (parameter.i.Name)
{
case "CancellationToken":
parametersInsertList.Add("(CancellationToken)special[1]");
break;
case "RequestContext":
parametersInsertList.Add("(RequestContext)special[0]");
break;
default:
parametersInsertList.Add(CodegenUtilities.Caster(parameter.i,
$"parameters[{parameter.Item2}]"));
break;
}
var parametersInsert = string.Join(", ", parametersInsertList);
var funcInvoking = $@"{{
{level} (i as {baseInterface.ToUnityString()}).{CodegenUtilities.EventExternalName(eventSymbol)}({parametersInsert});
{level} return null;
{level} }}";
var invokerTemplate = methodRepoTemplate.CloneInnerSyncInvoker();
invokerTemplate.DefineIsVoid("true");
invokerTemplate.DefineReturnType("void");
invokerTemplate.DefineParametersType(parameterTypes);
invokerTemplate.DefineSuitableType(baseInterface.ToUnityString());
invokerTemplate.DefineFuncInvoking(funcInvoking);
invokerTemplate.DefineIsTrusted("true");
var backend = invokerTemplate.Compile();
methodRepoTemplate.InsertInvoke(backend);
invokers.Add(backend);
}
private void GeneratePropertyMethod(MethodRepoTemplate methodRepoTemplate, IMethodSymbol method,
List<(string, IMethodSymbol)> propsCollection, List invokers, INamedTypeSymbol baseInterface)
{
string frontend;
string backend;
if (method.MethodKind == MethodKind.PropertyGet)
{
var parametersArray = "";
if (method.Parameters.Length != 0)
{
parametersArray =
$", new System.Object[] {{ {string.Join(", ", method.Parameters.Select(p => p.Name))} }}";
var parameterTypes = string.Join(", ",
$"{string.Join(", ", method.Parameters.Select(p => "typeof(" + p.Type.ToUnityString() + ")"))}");
var parameterInserts = string.Join(", ",
method.Parameters.Select(p =>
CodegenUtilities.Caster(p.Type, "parameters[" + method.Parameters.IndexOf(p) + "]")));
var invokerTemplate = methodRepoTemplate.CloneInnerSyncInvoker();
invokerTemplate.DefineIsVoid("false");
invokerTemplate.DefineReturnType(method.ReturnType.ToUnityString());
invokerTemplate.DefineParametersType(parameterTypes);
invokerTemplate.DefineSuitableType(baseInterface.ToUnityString());
invokerTemplate.DefineFuncInvoking($"(i as {method.ContainingType.ToUnityString()})[{parameterInserts}]");
invokerTemplate.DefineIsTrusted("true");
backend = invokerTemplate.Compile();
}
else
{
var invokerTemplate = methodRepoTemplate.CloneInnerSyncInvoker();
invokerTemplate.DefineIsVoid("false");
invokerTemplate.DefineReturnType(method.ReturnType.ToUnityString());
invokerTemplate.DefineParametersType("");
invokerTemplate.DefineSuitableType(baseInterface.ToUnityString());
invokerTemplate.DefineFuncInvoking($"(i as {method.ContainingType.ToUnityString()}).{(method.AssociatedSymbol as IPropertySymbol)!.Name}");
invokerTemplate.DefineIsTrusted("true");
backend = invokerTemplate.Compile();
}
frontend =
$"get => GetResultAsync<{method.ReturnType.ToUnityString()}>(CallIndices[{_currentInternalCallIndex++}]{parametersArray}).GetAwaiter().GetResult();";
}
else
{
var parametersArray = "value";
if (method.Parameters.Length != 1)
{
parametersArray = string.Join(", ", method.Parameters.Select(p => p.Name));
var parameterTypes = string.Join(", ",
$"{string.Join(", ", method.Parameters.Select(p => $"typeof({p.Type.ToUnityString()})"))}");
var parameterInserts = string.Join(", ",
method.Parameters.Take(method.Parameters.Length - 1).Select(p =>
CodegenUtilities.Caster(p.Type, "parameters[" + method.Parameters.IndexOf(p) + "]")));
var valueInsert = CodegenUtilities.Caster(method.Parameters.Last().Type,
"parameters[" + (method.Parameters.Length - 1) + "]");
var invokerTemplate = methodRepoTemplate.CloneInnerSyncInvoker();
invokerTemplate.DefineIsVoid("true");
invokerTemplate.DefineReturnType(method.ReturnType.ToUnityString());
invokerTemplate.DefineParametersType(parameterTypes);
invokerTemplate.DefineSuitableType(baseInterface.ToUnityString());
invokerTemplate.DefineFuncInvoking($"(i as {method.ContainingType.ToUnityString()})[{parameterInserts}] = {valueInsert}");
invokerTemplate.DefineIsTrusted("true");
backend = invokerTemplate.Compile();
}
else
{
var invokerTemplate = methodRepoTemplate.CloneInnerSyncInvoker();
invokerTemplate.DefineIsVoid("true");
invokerTemplate.DefineReturnType(method.ReturnType.ToUnityString());
invokerTemplate.DefineParametersType($"typeof({method.Parameters.First().Type.ToUnityString()})");
invokerTemplate.DefineSuitableType(baseInterface.ToUnityString());
invokerTemplate.DefineFuncInvoking($"(i as {method.ContainingType.ToUnityString()}).{(method.AssociatedSymbol as IPropertySymbol)!.Name} = {CodegenUtilities.Caster((method.AssociatedSymbol as IPropertySymbol)!.Type, "parameters[0]")}");
invokerTemplate.DefineIsTrusted("true");
backend = invokerTemplate.Compile();
}
frontend =
$"set => CallAsync(CallIndices[{_currentInternalCallIndex++}], new System.Object[] {{ {parametersArray} }}).Wait();";
}
propsCollection.Add((frontend, method));
methodRepoTemplate.InsertInvoke(backend);
invokers.Add(backend);
}
}
public static class CodegenUtilities
{
public static string EventExternalName(IEventSymbol eventSymbol)
{
return $"{eventSymbol.Name}External";
}
public static string Caster(ITypeSymbol type, string inner)
{
if (!type.IsValueType)
return inner +
" as " +
type.ToUnityString();
return $"({type.ToUnityString()})" + inner;
}
public static string JoinWithComa(IEnumerable parts)
{
return string.Join(", ", parts);
}
public static string ToFullString(IParameterSymbol parameter)
{
return parameter.Type.ToUnityString() + " " + parameter.Name;
}
public static string ToFullString(ITypeSymbol type)
{
return type.ToUnityString();
}
public static string ExtractTaskType(ITypeSymbol taskType)
{
var generics = ((INamedTypeSymbol)taskType).TypeArguments;
return generics.Length == 0 ? "void" : generics[0].ToUnityString();
}
public static List CollectMembers(INamedTypeSymbol type)
{
var methods = type.GetMembers().OfType().ToList();
foreach (var inner in type.AllInterfaces) methods.AddRange(inner.GetMembers().OfType());
methods.RemoveAll(m => m.Name == "Dispose");
return methods.OrderBy(i => i.Name).ToList();
}
}
public static class CodegenExtensions
{
public static string ToUnityString(this ITypeSymbol type)
{
var parts = type.ToDisplayParts();
return parts.Any(i => i.Kind == SymbolDisplayPartKind.Keyword)
? parts.ToUnityString()
: type.ToDisplayString();
}
public static string ToUnityString(this IParameterSymbol parameter)
{
return parameter.Type.ToUnityString() + " " + parameter.Name;
}
private static string ToUnityString(this ImmutableArray parts)
{
var sb = new StringBuilder();
foreach (var displayPart in parts)
if (displayPart.Kind == SymbolDisplayPartKind.Keyword)
{
if (displayPart.ToString() == "void")
{
sb.Append(displayPart.ToString());
continue;
}
sb.Append($"{displayPart.Symbol!.ContainingNamespace.ToDisplayString()}.{displayPart.Symbol.Name}");
}
else
{
sb.Append(displayPart.ToString());
}
return sb.ToString();
}
}
}