/* Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. */ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; using System.Linq; #nullable enable namespace Yarn.Unity.ActionAnalyser { public struct Position { public int Line; public int Column; } public struct Range { public Position Start; public Position End; public static implicit operator Range(FileLinePositionSpan span) { var start = span.StartLinePosition; var end = span.EndLinePosition; return new Range { Start = { Line = start.Line + 1, Column = start.Character + 1, }, End = { Line = end.Line + 1, Column = end.Character + 1, }, }; } } public enum ActionType { /// /// The method represents a command. /// Command, /// /// The method represents a function. /// Function, /// /// The method may have been intended to be an action, but its type /// cannot be determined. /// Invalid, /// /// The method is not a Yarn action. /// NotAnAction, } public enum DeclarationType { /// /// The action is declared via a YarnCommand or YarnFunction attribute. /// Attribute, /// /// The action is declared by calling AddCommandHandler or AddFunction /// on a DialogueRunner. /// DirectRegistration } public enum AsyncType { /// /// The action operates synchronously. /// Sync, /// /// The action may operate asynchronously, and Dialogue Runners should /// check the return value of the action to determine whether to block /// on the method call or not. /// /// /// This is only valid for objects whose is . /// MaybeAsyncCoroutine, /// /// The action operates asynchronously using a coroutine. /// AsyncCoroutine, /// /// The action operates asynchronously through c# async infrastructure /// AsyncTask, } static class ITypeSymbolExtension { public static string GetYarnTypeString(this ITypeSymbol typeSymbol) { return typeSymbol.SpecialType switch { SpecialType.System_Boolean => "bool", SpecialType.System_SByte => "number", SpecialType.System_Byte => "number", SpecialType.System_Int16 => "number", SpecialType.System_UInt16 => "number", SpecialType.System_Int32 => "number", SpecialType.System_UInt32 => "number", SpecialType.System_Int64 => "number", SpecialType.System_UInt64 => "number", SpecialType.System_Decimal => "number", SpecialType.System_Single => "number", SpecialType.System_Double => "number", SpecialType.System_String => "string", _ => "any" }; } } public struct Parameter { public bool IsOptional; public string Name; public ITypeSymbol Type; public string? Description; public string? DefaultValueString; public bool IsParamsArray; public AttributeData[]? Attributes; public readonly string YarnTypeString => Type.GetYarnTypeString(); // this should change to support the subtypes through the same logic we use below, for now it's fine } public class Action { public Action(string name, ActionType type, IMethodSymbol methodSymbol) { Name = name; Type = type; MethodSymbol = methodSymbol; } /// /// The name of this action. /// public string Name { get; internal set; } /// /// The method symbol for this action. /// public IMethodSymbol MethodSymbol { get; internal set; } public string? Description { get; internal set; } /// /// The declaration of this action's method, if available. /// public SyntaxNode? Declaration { get; internal set; } /// /// The type of the action. /// public ActionType Type { get; internal set; } /// /// The declaration type of the action. /// public DeclarationType DeclarationType { get; internal set; } /// /// The sync/async type of the action. /// public AsyncType AsyncType { get; internal set; } /// /// The that can be /// used to answer semantic queries about this method. /// internal SemanticModel? SemanticModel { get; set; } /// /// The fully-qualified name for this method, including the global /// prefix. /// public string? MethodName { get; set; } /// /// Gets the short form of the method, essentially the easy to read form of . /// public string? MethodIdentifierName { get; internal set; } /// /// Whether this action is a static method, or an instance method. /// public bool IsStatic { get; internal set; } /// /// Gets the path to the file that this action was declared in. /// public string? SourceFileName { get; internal set; } /// /// The syntax node for the method declaration associated with this action. /// public SyntaxNode? MethodDeclarationSyntax { get; internal set; } // The names of the methods that register commands and functions private const string AddCommandHandlerMethodName = "AddCommandHandler"; private const string AddFunctionMethodName = "AddFunction"; private const string RegisterFunctionDeclarationName = "RegisterFunctionDeclaration"; /// /// The list of parameters that this action takes. /// public List Parameters = new List(); public string? ReturnDescription; public string YarnReturnTypeString => this.MethodSymbol.ReturnType.GetYarnTypeString(); public bool ContainsErrors = false; public string ToJSON() { var result = new Dictionary(); result["yarnName"] = this.Name; result["definitionName"] = this.MethodName; result["fileName"] = this.SourceFileName; if (!string.IsNullOrEmpty(this.Description)) { result["documentation"] = this.Description; } result["language"] = "csharp"; result["async"] = this.AsyncType != AsyncType.Sync; result["containsErrors"] = this.ContainsErrors; if (this.Declaration != null) { var location = this.Declaration.GetLocation().GetLineSpan(); var startPosition = new Dictionary() { {"line", location.StartLinePosition.Line}, {"character", location.StartLinePosition.Character}, }; var endPosition = new Dictionary() { {"line", location.EndLinePosition.Line}, {"character", location.EndLinePosition.Character}, }; result["location"] = new Dictionary>() { {"start", startPosition}, {"end", endPosition}, }; } result["parameters"] = new List>(this.Parameters.Select(p => { var paramObject = new Dictionary(); paramObject["name"] = p.Name; if (!string.IsNullOrEmpty(p.Description)) { paramObject["documentation"] = p.Description; } if (!string.IsNullOrEmpty(p.DefaultValueString)) { paramObject["defaultValue"] = p.DefaultValueString; } paramObject["isParamsArray"] = p.IsParamsArray; // there are two special cases for parameters // if it is a subclass of UnityEngine.Component or MonoBehaviour we additionally add the subtype // this is used by the editor later on to let the writer know WHERE the command will be going // otherwise we just add the Yarn type of the parameter if (p.Type.BaseType?.Name == "MonoBehaviour" || p.Type.BaseType?.Name == "Component") { paramObject["type"] = "instance"; paramObject["subtype"] = p.Type.Name; } else { // there are two special case of the regular types: // if you are a string and attributed as a node parameter you get declared as being a node type // if you have an enum attribute it gets declared as an enum and it has the subtype as defined in the enum attribute var isANodeType = p.Attributes?.Count(a => a.AttributeClass?.Name == "YarnNodeParameterAttribute") > 0; var isAnEnum = p.Attributes?.Count(a => a.AttributeClass?.Name == "YarnEnumParameterAttribute") > 0; if (isANodeType && p.Type.SpecialType == SpecialType.System_String) { paramObject["type"] = "node"; } else if (isAnEnum) { var subtype = "any"; var attribute = p.Attributes?.Where(a => a.AttributeClass?.Name == "YarnEnumParameterAttribute").First(); if (attribute != null && attribute.ConstructorArguments.Count() > 0) { var enumType = attribute.ConstructorArguments[0]; if (enumType.Type?.SpecialType == SpecialType.System_String) { subtype = enumType.Value as string ?? p.YarnTypeString; } } paramObject["type"] = "enum"; paramObject["subtype"] = subtype; } else { paramObject["type"] = p.YarnTypeString; } } return paramObject; }).ToArray()); if (this.Type == ActionType.Function) { var retvrn = new Dictionary(); retvrn["type"] = this.YarnReturnTypeString; if (!string.IsNullOrWhiteSpace(this.ReturnDescription)) { retvrn["description"] = this.ReturnDescription!; } result["return"] = retvrn; } return Yarn.Unity.Editor.Json.Serialize(result); } public List Validate(Compilation compilation, ILogger? logger) { logger?.WriteLine($"Beginning validation"); var diagnostics = new List(); if (this.MethodDeclarationSyntax == null) { // No declaration syntax - we have nowhere to attach any diagnostics to return diagnostics; } Location diagnosticLocation; string identifier; if (this.Declaration is MethodDeclarationSyntax methodDeclarationSyntax) { diagnosticLocation = methodDeclarationSyntax.Identifier.GetLocation(); identifier = methodDeclarationSyntax.Identifier.ToString(); } else { diagnosticLocation = this.MethodDeclarationSyntax.GetLocation(); identifier = "(anonymous function)"; } // Commands are parsed as whitespace, so spaces in the command name // would render the command un-callable. if (Name.Any(x => Char.IsWhiteSpace(x))) { diagnostics.Add(Diagnostic.Create(Diagnostics.YS1002ActionMethodsMustHaveAValidName, this.MethodDeclarationSyntax.GetLocation(), this.Name)); } if (this.Name == null) { throw new NullReferenceException("Action name is null"); } if (this.MethodSymbol == null) { throw new NullReferenceException($"Method symbol for {Name} is null"); } // Actions that are registered via an attribute must be publicly // accessible if (this.DeclarationType == DeclarationType.Attribute) { if (MethodSymbol.DeclaredAccessibility != Accessibility.Public) { // The method is not public diagnostics.Add(Diagnostic.Create( Diagnostics.YS1001ActionMethodsMustBePublic, diagnosticLocation, identifier, MethodSymbol.DeclaredAccessibility)); } else { var containingType = MethodSymbol.ContainingType; while (containingType != null) { if (containingType.DeclaredAccessibility != Accessibility.Public) { // The method is public, but it's within a type that // is not var typeName = containingType.Name ?? "(anonymous)"; diagnostics.Add(Diagnostic.Create( Diagnostics.YS1001ActionMethodsMustBePublic, diagnosticLocation, identifier, typeName, containingType.DeclaredAccessibility)); break; } containingType = containingType.ContainingType; } } } switch (Type) { case ActionType.Invalid: { var actionAttributes = MethodSymbol.GetAttributes().Where(attr => Analyser.IsAttributeYarnCommand(attr)); var count = actionAttributes.Count(); if (count != 1) { diagnostics.Add(Diagnostic.Create(Diagnostics.YS1005ActionMethodsMustHaveOneActionAttribute, diagnosticLocation, 0)); } else { diagnostics.Add(Diagnostic.Create(Diagnostics.YS1000UnknownError, diagnosticLocation, "Method marked as 'not an action' but it had one attribute")); } } break; case ActionType.Command: diagnostics.AddRange(ValidateCommand(compilation, logger)); break; case ActionType.Function: diagnostics.AddRange(ValidateFunction(compilation, logger)); break; default: diagnostics.Add(Diagnostic.Create(Diagnostics.YS1000UnknownError, diagnosticLocation, $"Internal error: invalid type {Type}")); break; } return diagnostics; } private IEnumerable ValidateFunction(Compilation compilation, ILogger? logger) { string identifier; Location returnTypeLocation; Location identifierLocation; if (this.Declaration == null) { // No declaration - we can't attach any diagnostics yield break; } if (this.Declaration is MethodDeclarationSyntax methodDeclarationSyntax) { identifierLocation = methodDeclarationSyntax.Identifier.GetLocation(); returnTypeLocation = methodDeclarationSyntax.ReturnType.GetLocation(); identifier = methodDeclarationSyntax.Identifier.ToString(); } else { identifierLocation = Declaration.GetLocation(); returnTypeLocation = this.Declaration.GetLocation(); identifier = "(anonymous function)"; } if (this.MethodSymbol == null) { throw new NotImplementedException("Todo: handle case where action's method is not a IMethodSymbol"); } // Functions must be static if they're declared via attributes if (this.DeclarationType == DeclarationType.Attribute && this.MethodSymbol.MethodKind == MethodKind.Ordinary && this.MethodSymbol.IsStatic == false) { yield return Diagnostic.Create(Diagnostics.YS1006YarnFunctionsMustBeStatic, identifierLocation); } logger?.Inc(); logger?.WriteLine($"Validating {identifier} as a function"); var paramDiags = ValidateParameters(compilation, logger); foreach (var p in paramDiags) { yield return p; } // Functions must return a number, string, or bool var returnTypeSymbol = this.MethodSymbol.ReturnType; logger?.Dec(); switch (returnTypeSymbol.SpecialType) { case SpecialType.System_Boolean: case SpecialType.System_SByte: case SpecialType.System_Byte: case SpecialType.System_Int16: case SpecialType.System_UInt16: case SpecialType.System_Int32: case SpecialType.System_UInt32: case SpecialType.System_Int64: case SpecialType.System_UInt64: case SpecialType.System_Decimal: case SpecialType.System_Single: case SpecialType.System_Double: case SpecialType.System_String: break; default: yield return Diagnostic.Create(Diagnostics.YS1004FunctionMethodsMustHaveAValidReturnType, returnTypeLocation, identifier, returnTypeSymbol.ToString()); break; } } // validates the parameters are correct private List ValidateParameters(Compilation compilation, ILogger? logger) { logger?.Inc(); List diagnostics = new List(); ParameterListSyntax? parameterList = null; string? identifier = null; if (this.MethodDeclarationSyntax is MethodDeclarationSyntax methodDeclaration) { identifier = methodDeclaration.Identifier.ToString(); logger?.WriteLine($"identified {identifier} as a method"); parameterList = methodDeclaration.ParameterList; } else if (this.MethodDeclarationSyntax is LocalFunctionStatementSyntax localFunctionStatement) { identifier = localFunctionStatement.Identifier.ToString(); logger?.WriteLine($"identified {identifier} as a local function"); parameterList = localFunctionStatement.ParameterList; } else if (this.MethodDeclarationSyntax is LambdaExpressionSyntax lambdaExpression) { logger?.WriteLine("identifed the action as a lambda."); var actionLocation = lambdaExpression.GetLocation(); if (lambdaExpression is SimpleLambdaExpressionSyntax) { logger?.WriteLine("The action is a simple lambda, validations do not apply here, skipping this action."); logger?.Dec(); diagnostics.Add(Diagnostic.Create(Diagnostics.YS1012ActionIsALambda, actionLocation)); return diagnostics; } if (lambdaExpression is ParenthesizedLambdaExpressionSyntax pls) { logger?.WriteLine("The action is a parenthesized lambda, can perform some validation."); identifier = "(lambda expression)"; parameterList = pls.ParameterList; diagnostics.Add(Diagnostic.Create(Diagnostics.YS1012ActionIsALambda, actionLocation)); } } if (parameterList == null || parameterList.Parameters.Count() == 0) { logger?.WriteLine($"{identifier} has no parameters, ignoring"); logger?.Dec(); return diagnostics; } logger?.WriteLine($"Will be checking {parameterList.Parameters.Count()} parameters"); int parameterIndex = 0; int parameterCount = parameterList.Parameters.Count; foreach (var parameter in parameterList.Parameters) { parameterIndex += 1; logger?.Inc(); if (parameter.Type == null) { logger?.WriteLine($"{parameter.ToFullString()} has no type, ignoring validation?"); logger?.Dec(); continue; } var model = compilation.GetSemanticModel(parameter.SyntaxTree); var typeInfo = model.GetTypeInfo(parameter.Type).Type; var parameterName = model.GetDeclaredSymbol(parameter)?.Name ?? "(UNKNOWN)"; logger?.WriteLine($"Validating {parameterName}"); if (typeInfo == null) { logger?.WriteLine($"Unable to determine typeinfo of {parameterName} ignoring validation?"); logger?.Dec(); continue; } var symbol = model.GetDeclaredSymbol(parameter); if (symbol == null) { logger?.WriteLine($"Unable to determine the declared symbol for {parameterName}, skipping validation"); logger?.Dec(); continue; } // Params arrays or arrays that are the final parameter make // that parameter variadic in Yarn Spinner. Check that the // element type of that array is of the right type. if (symbol.Type is IArrayTypeSymbol arrayTypeSymbol && (symbol.IsParams || parameterIndex == parameterCount)) { var subtype = arrayTypeSymbol.ElementType; if (subtype.GetYarnTypeString() == "any") { logger?.WriteLine($"{parameterName} is a parameter array of non Yarn compatible types!"); diagnostics.Add(Diagnostic.Create(Diagnostics.YS1008ActionsParamsArraysMustBeOfYarnTypes, parameter.GetLocation(), parameterName, subtype.Name)); } } else { if (typeInfo.GetYarnTypeString() == "any" && typeInfo.BaseType?.Name != "Component") { // we have an invalid type logger?.WriteLine($"{parameterName} is an invalid type for use in a Yarn action"); diagnostics.Add(Diagnostic.Create(Diagnostics.YS1011ActionsParameterIsAnIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name)); } } foreach (var attribute in symbol.GetAttributes()) { // this attribute is an enum parameter if (attribute.AttributeClass?.Name == "YarnEnumParameterAttribute") { if (typeInfo.GetYarnTypeString() == "any") { logger?.WriteLine($"{parameterName} is attributed as an enum but isn't a Yarn compatible type!"); diagnostics.Add(Diagnostic.Create(Diagnostics.YS1009ActionsEnumAttributedParameterIsOfIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name)); } } if (attribute.AttributeClass?.Name == "YarnNodeParameterAttribute") { if (typeInfo.GetYarnTypeString() != "string") { logger?.WriteLine($"{parameterName} is attributed as a node but isn't a string!"); diagnostics.Add(Diagnostic.Create(Diagnostics.YS1010ActionsNodeAttributedParameterIsOfIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name)); } } } logger?.Dec(); } logger?.Dec(); return diagnostics; } private IEnumerable ValidateCommand(Compilation compilation, ILogger? logger) { logger?.Inc(); if (MethodSymbol == null) { logger?.Dec(); throw new NullReferenceException("Method symbol is null"); } List validCommandReturnTypes = new List { compilation.GetTypeByMetadataName("UnityEngine.Coroutine"), compilation.GetTypeByMetadataName("System.Collections.IEnumerator"), compilation.GetSpecialType(SpecialType.System_Void), } .NonNull(throwIfAnyNull: true) .ToList(); List validTaskTypes = new List { compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"), compilation.GetTypeByMetadataName("Cysharp.Threading.Tasks.UniTask"), compilation.GetTypeByMetadataName("UnityEngine.Awaitable"), compilation.GetTypeByMetadataName("Yarn.Unity.YarnTask"), }.NonNull(throwIfAnyNull: false) .ToList(); // Explicitly ban 'string' as a return type - strings implement // IEnumerator, but they're not coroutines. We'll need to manually // exclude this. List knownInvalidCommandReturnTypes = new List { compilation.GetSpecialType(SpecialType.System_String), } .NonNull(throwIfAnyNull: true) .ToList(); // Functions must return void, IEnumerator, Coroutine, or an awaitable type var returnTypeSymbol = MethodSymbol.ReturnType; Location returnTypeLocation; string identifier; string returnTypeName; if (this.MethodDeclarationSyntax is MethodDeclarationSyntax methodDeclaration) { returnTypeLocation = methodDeclaration.ReturnType.GetLocation(); identifier = methodDeclaration.Identifier.ToString(); returnTypeName = methodDeclaration.ReturnType.ToString(); } else if (this.MethodDeclarationSyntax is LocalFunctionStatementSyntax localFunctionStatement) { returnTypeLocation = localFunctionStatement.ReturnType.GetLocation(); identifier = localFunctionStatement.Identifier.ToString(); returnTypeName = localFunctionStatement.ReturnType.ToString(); } else if (this.MethodDeclarationSyntax is LambdaExpressionSyntax lambdaExpression) { returnTypeLocation = lambdaExpression.GetLocation(); identifier = "(lambda expression)"; returnTypeName = returnTypeSymbol.Name; } else { logger?.Dec(); throw new InvalidOperationException($"Expected decl for {this.Name} ({this.SourceFileName}) was of unexpected type {this.MethodDeclarationSyntax?.GetType().Name ?? "null"}"); } logger?.WriteLine($"Validating {identifier} as a command"); var paramDiags = ValidateParameters(compilation, logger); foreach (var p in paramDiags) { yield return p; } var typeIsKnownValid = validCommandReturnTypes.Contains(returnTypeSymbol) || validTaskTypes.Contains(returnTypeSymbol); var typeIsKnownInvalid = knownInvalidCommandReturnTypes.Contains(returnTypeSymbol); var returnTypeIsValid = typeIsKnownValid && !typeIsKnownInvalid; logger?.Dec(); if (returnTypeIsValid == false) { yield return Diagnostic.Create(Diagnostics.YS1003CommandMethodsMustHaveAValidReturnType, returnTypeLocation, identifier, returnTypeName); } } public StatementSyntax GetRegistrationSyntax(string dialogueRunnerVariableName = "dialogueRunner") { if (MethodSymbol == null) { throw new NullReferenceException("Method symbol is null"); } if (Name == null) { throw new NullReferenceException("Action name is null"); } string registrationMethodName; switch (Type) { case ActionType.Command: registrationMethodName = AddCommandHandlerMethodName; break; case ActionType.Function: registrationMethodName = AddFunctionMethodName; break; default: throw new InvalidOperationException($"Action {Name} is not a valid action"); } SimpleNameSyntax nameSyntax; // Get any parameters we have for this method as a sequence of type // symbols. We'll use that when building the call to // AddCommandHandler/Function. var parameterTypes = (MethodSymbol as IMethodSymbol)?.Parameters.Select(p => p.Type) ?? Enumerable.Empty(); var typeArguments = parameterTypes.Select(t => { return SyntaxFactory.ParseTypeName(t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); }); // If this is a function, we also need to include the return type in // this list. if (Type == ActionType.Function) { var returnType = (MethodSymbol as IMethodSymbol)?.ReturnType ?? throw new InvalidOperationException($"Action {Name} has type {ActionType.Function}, but its return type is null."); typeArguments = typeArguments.Append(SyntaxFactory.ParseTypeName(returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); } if (typeArguments.Any() && MethodSymbol?.IsStatic == true) { // This method needs to be specified with type arguments, so // we'll need to call the appropriate generic version of // AddCommandHandler/Function that takes type parameters. Create // a new GenericName for AddCommandHandler/Function and provide // it with the type parameter list that we just built. nameSyntax = SyntaxFactory.GenericName( SyntaxFactory.Identifier(registrationMethodName), SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(typeArguments)) ); } else { // This method doesn't need to specify any type parameters, so // we can just use the identifier name. nameSyntax = SyntaxFactory.IdentifierName(registrationMethodName); } // Create the expression that refers to the // 'AddCommandHandler/Function' instance method on the dialogue // runner variable name we were provided. var addCommandHandlerExpression = SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName(dialogueRunnerVariableName), SyntaxFactory.Token(SyntaxKind.DotToken), nameSyntax ); ExpressionSyntax methodReferenceExpression = GetReferenceSyntaxForRegistration(); var arguments = SyntaxFactory.ArgumentList().AddArguments(new[]{ SyntaxFactory.Argument( SyntaxFactory.LiteralExpression( SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(this.Name) ) ), SyntaxFactory.Argument(methodReferenceExpression) .WithLeadingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " ")), }); var invocationExpressionSyntax = SyntaxFactory.InvocationExpression(addCommandHandlerExpression, arguments); var invocationStatement = SyntaxFactory.ExpressionStatement(invocationExpressionSyntax); return invocationStatement; } public ExpressionSyntax GetReferenceSyntaxForRegistration() { // Create an expression that refers to the type that contains the // method we're registering. var containingTypeExpression = SyntaxFactory.ParseName(MethodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); // Now use that to create an expression that refers to _this method group_ // on _that type_. var methodReference = SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, containingTypeExpression, SyntaxFactory.IdentifierName(MethodSymbol.Name) ); if (IsStatic) { // If the method is static, we can use the reference to the method directly. return methodReference; } else { // If the method is not static, we must create a MethodInfo for this method, like this: // typeof(ContainingType) // .GetMethod(nameof(ContainingType.Method), // new[] { typeof(MethodParam1), typeof(MethodParam2)} ) // Create an expression that gets a MethodInfo for the action's method. var typeOfContainingTypeExpression = SyntaxFactory.TypeOfExpression(containingTypeExpression); const string nameOfIdentifier = "nameof"; const string getMethodIdentifier = "GetMethod"; var typeOfMethodParameters = MethodSymbol.Parameters.Select(p => { string typeName = p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); TypeSyntax type = SyntaxFactory.ParseTypeName(typeName); return SyntaxFactory.TypeOfExpression(type); }); ExpressionSyntax nameOfMethod; if (MethodSymbol.DeclaredAccessibility != Accessibility.Public) { // The method is not public, so we can't use nameof() on it, // because it would cause a compiler error. Instead, we'll have to // refer to the method by name. nameOfMethod = SyntaxFactory.LiteralExpression( SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(MethodName ?? MethodSymbol.Name) ); } else { // The method is public, so we can use nameof() to refer to // it in a more durable way. nameOfMethod = SyntaxFactory.InvocationExpression( SyntaxFactory.ParseName(nameOfIdentifier), SyntaxFactory.ArgumentList( SyntaxFactory.SeparatedList( new[] { SyntaxFactory.Argument(methodReference) } ) ) ); } var arrayOfTypeParameters = SyntaxFactory.ArrayCreationExpression( SyntaxFactory.ArrayType( SyntaxFactory.ParseTypeName("System.Type"), SyntaxFactory.List( new[] { SyntaxFactory.ArrayRankSpecifier( SyntaxFactory.SingletonSeparatedList( SyntaxFactory.OmittedArraySizeExpression() ) ) } ) ), SyntaxFactory.InitializerExpression( SyntaxKind.ArrayInitializerExpression, SyntaxFactory.SeparatedList( typeOfMethodParameters ) ) ); var getMethod = SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, typeOfContainingTypeExpression, SyntaxFactory.IdentifierName(getMethodIdentifier) ); var getMethodArguments = SyntaxFactory.ArgumentList( SyntaxFactory.SeparatedList( new[] { SyntaxFactory.Argument(nameOfMethod), SyntaxFactory.Argument(arrayOfTypeParameters) } ) ); var getMethodInvocation = SyntaxFactory.InvocationExpression(getMethod, getMethodArguments); return getMethodInvocation; } } public StatementSyntax GetFunctionDeclarationSyntax(string dialogueRunnerVariableName = "dialogueRunner") { var typeOfMethodReturn = SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(MethodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); var typeOfMethodParameters = MethodSymbol.Parameters.Select(p => { string typeName = p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); TypeSyntax type = SyntaxFactory.ParseTypeName(typeName); return SyntaxFactory.TypeOfExpression(type); }); var arrayOfTypeParameters = SyntaxFactory.ArrayCreationExpression( SyntaxFactory.ArrayType( SyntaxFactory.ParseTypeName("System.Type"), SyntaxFactory.List( new[] { SyntaxFactory.ArrayRankSpecifier( SyntaxFactory.SingletonSeparatedList( SyntaxFactory.OmittedArraySizeExpression() ) ) } ) ), SyntaxFactory.InitializerExpression( SyntaxKind.ArrayInitializerExpression, SyntaxFactory.SeparatedList( typeOfMethodParameters ) ) ); var argumentsToRegisterCall = SyntaxFactory.ArgumentList().AddArguments(new[]{ SyntaxFactory.Argument( SyntaxFactory.LiteralExpression( SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(this.Name) ) ), SyntaxFactory.Argument(typeOfMethodReturn), SyntaxFactory.Argument(arrayOfTypeParameters) }); // Create the expression that refers to the // 'RegisterFunctionDeclaration' instance method on the dialogue // runner variable name we were provided. var registerFunctionMethodAccess = SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName(dialogueRunnerVariableName), SyntaxFactory.Token(SyntaxKind.DotToken), SyntaxFactory.IdentifierName(RegisterFunctionDeclarationName) ); var registerFunctionMethodInvocation = SyntaxFactory.InvocationExpression(registerFunctionMethodAccess, argumentsToRegisterCall); var invocationStatement = SyntaxFactory.ExpressionStatement(registerFunctionMethodInvocation); return invocationStatement; } } }