Files
Cielonos/Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs
SoulliesOfficial 8186f54e90 新场景,剧情
2026-06-02 12:55:39 -04:00

1026 lines
42 KiB
C#

/*
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
{
/// <summary>
/// The method represents a command.
/// </summary>
Command,
/// <summary>
/// The method represents a function.
/// </summary>
Function,
/// <summary>
/// The method may have been intended to be an action, but its type
/// cannot be determined.
/// </summary>
Invalid,
/// <summary>
/// The method is not a Yarn action.
/// </summary>
NotAnAction,
}
public enum DeclarationType
{
/// <summary>
/// The action is declared via a YarnCommand or YarnFunction attribute.
/// </summary>
Attribute,
/// <summary>
/// The action is declared by calling AddCommandHandler or AddFunction
/// on a DialogueRunner.
/// </summary>
DirectRegistration
}
public enum AsyncType
{
/// <summary>
/// The action operates synchronously.
/// </summary>
Sync,
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This is only valid for <see cref="Action"/> objects whose <see
/// cref="Action.Type"/> is <see cref="ActionType.Command"/>.
/// </remarks>
MaybeAsyncCoroutine,
/// <summary>
/// The action operates asynchronously using a coroutine.
/// </summary>
AsyncCoroutine,
/// <summary>
/// The action operates asynchronously through c# async infrastructure
/// </summary>
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;
}
/// <summary>
/// The name of this action.
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// The method symbol for this action.
/// </summary>
public IMethodSymbol MethodSymbol { get; internal set; }
public string? Description { get; internal set; }
/// <summary>
/// The declaration of this action's method, if available.
/// </summary>
public SyntaxNode? Declaration { get; internal set; }
/// <summary>
/// The type of the action.
/// </summary>
public ActionType Type { get; internal set; }
/// <summary>
/// The declaration type of the action.
/// </summary>
public DeclarationType DeclarationType { get; internal set; }
/// <summary>
/// The sync/async type of the action.
/// </summary>
public AsyncType AsyncType { get; internal set; }
/// <summary>
/// The <see cref="Microsoft.CodeAnalysis.SemanticModel"/> that can be
/// used to answer semantic queries about this method.
/// </summary>
internal SemanticModel? SemanticModel { get; set; }
/// <summary>
/// The fully-qualified name for this method, including the global
/// prefix.
/// </summary>
public string? MethodName { get; set; }
/// <summary>
/// Gets the short form of the method, essentially the easy to read form of <see cref="MethodName"/>.
/// </summary>
public string? MethodIdentifierName { get; internal set; }
/// <summary>
/// Whether this action is a static method, or an instance method.
/// </summary>
public bool IsStatic { get; internal set; }
/// <summary>
/// Gets the path to the file that this action was declared in.
/// </summary>
public string? SourceFileName { get; internal set; }
/// <summary>
/// The syntax node for the method declaration associated with this action.
/// </summary>
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";
/// <summary>
/// The list of parameters that this action takes.
/// </summary>
public List<Parameter> Parameters = new List<Parameter>();
public string? ReturnDescription;
public string YarnReturnTypeString => this.MethodSymbol.ReturnType.GetYarnTypeString();
public bool ContainsErrors = false;
public string ToJSON()
{
var result = new Dictionary<string, object?>();
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<string, int>()
{
{"line", location.StartLinePosition.Line},
{"character", location.StartLinePosition.Character},
};
var endPosition = new Dictionary<string, int>()
{
{"line", location.EndLinePosition.Line},
{"character", location.EndLinePosition.Character},
};
result["location"] = new Dictionary<string, Dictionary<string, int>>()
{
{"start", startPosition},
{"end", endPosition},
};
}
result["parameters"] = new List<Dictionary<string, object?>>(this.Parameters.Select(p =>
{
var paramObject = new Dictionary<string, object?>();
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<string, string>();
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<Microsoft.CodeAnalysis.Diagnostic> Validate(Compilation compilation, ILogger? logger)
{
logger?.WriteLine($"Beginning validation");
var diagnostics = new List<Microsoft.CodeAnalysis.Diagnostic>();
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<Diagnostic> 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<Diagnostic> ValidateParameters(Compilation compilation, ILogger? logger)
{
logger?.Inc();
List<Diagnostic> diagnostics = new List<Diagnostic>();
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<Diagnostic> ValidateCommand(Compilation compilation, ILogger? logger)
{
logger?.Inc();
if (MethodSymbol == null)
{
logger?.Dec();
throw new NullReferenceException("Method symbol is null");
}
List<ITypeSymbol> validCommandReturnTypes = new List<ITypeSymbol?> {
compilation.GetTypeByMetadataName("UnityEngine.Coroutine"),
compilation.GetTypeByMetadataName("System.Collections.IEnumerator"),
compilation.GetSpecialType(SpecialType.System_Void),
}
.NonNull(throwIfAnyNull: true)
.ToList();
List<ITypeSymbol> validTaskTypes = new List<ITypeSymbol?> {
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<ITypeSymbol> knownInvalidCommandReturnTypes = new List<ITypeSymbol?> {
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<ITypeSymbol>();
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<ExpressionSyntax>(
SyntaxFactory.OmittedArraySizeExpression()
)
)
}
)
),
SyntaxFactory.InitializerExpression(
SyntaxKind.ArrayInitializerExpression,
SyntaxFactory.SeparatedList<ExpressionSyntax>(
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<ExpressionSyntax>(
SyntaxFactory.OmittedArraySizeExpression()
)
)
}
)
),
SyntaxFactory.InitializerExpression(
SyntaxKind.ArrayInitializerExpression,
SyntaxFactory.SeparatedList<ExpressionSyntax>(
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;
}
}
}