RobotNet/RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs
2025-10-15 15:15:53 +07:00

708 lines
30 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using RobotNet.Script;
using RobotNet.ScriptManager.Models;
using System.Collections.Immutable;
using System.Text;
using System.Text.RegularExpressions;
namespace RobotNet.ScriptManager.Helpers;
public static class CSharpSyntaxHelper
{
public static bool ResolveValueFromString(string valueStr, Type type, out object? value)
{
// Check if type is in MissionParameterTypes
if (!MissionParameterTypes.Contains(type))
{
value = null;
return false;
}
// If type is RobotNet.Script.IRobot, return null
if (type == RobotType)
{
value = null;
return true;
}
// Convert string to the corresponding type
if (type == typeof(string))
{
value = valueStr;
return true;
}
if (type.IsEnum)
{
value = Enum.Parse(type, valueStr, ignoreCase: true);
return true;
}
// Handle nullable types
var underlyingType = Nullable.GetUnderlyingType(type);
if (underlyingType != null)
{
value = null;
return false;
}
value = Convert.ChangeType(valueStr, type);
if (value is null)
{
return false;
}
else
{
return true;
}
}
public static bool VerifyScript(string code, out string error,
out List<ScriptVariableSyntax> variables,
out List<ScriptTaskData> tasks,
out List<ScriptMissionData> missions)
{
try
{
var listVariables = new List<ScriptVariableSyntax>();
var wrappedCode = string.Join(Environment.NewLine, [ScriptConfiguration.UsingNamespacesScript, "public class DummyClass", "{", ScriptConfiguration.DeveloptGlobalsScript, code, "}"]);
var devCompilation = CSharpCompilation.Create("CodeAnalysis")
.WithReferences(ScriptConfiguration.MetadataReferences)
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(wrappedCode));
var diagnostics = devCompilation.GetDiagnostics();
if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))
{
var message = "Verify errors: \r\n";
foreach (var diag in diagnostics)
{
if (diag.Severity == DiagnosticSeverity.Error)
{
message += $"\t❌ Error: {diag.GetMessage()} at {diag.Location}\r\n";
}
else if (diag.Severity == DiagnosticSeverity.Warning)
{
message += $"\t⚠ Warning: {diag.GetMessage()} at {diag.Location}\r\n";
}
}
throw new Exception(message);
}
error = "";
wrappedCode = string.Join(Environment.NewLine, [ScriptConfiguration.UsingNamespacesScript, "public class DummyClass", "{", code, "}"]);
var syntaxTree = CSharpSyntaxTree.ParseText(wrappedCode);
var root = syntaxTree.GetCompilationUnitRoot();
var runCompilation = CSharpCompilation.Create("CodeAnalysis")
.AddSyntaxTrees(syntaxTree)
.WithReferences(ScriptConfiguration.MetadataReferences)
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var classNode = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault(c => c.Identifier.Text.Equals("DummyClass")) ?? throw new Exception("No class named 'DummyClass' found in the script.");
var semanticModel = runCompilation.GetSemanticModel(syntaxTree);
GetScriptVariables(classNode, semanticModel, out variables);
GetScriptTasksAndMissions(classNode, semanticModel, out tasks, out missions);
return true;
}
catch (Exception ex)
{
error = $"An error occurred while verifying the script: {ex.Message}";
variables = [];
tasks = [];
missions = [];
return false;
}
finally
{
GC.Collect();
}
}
private static void GetScriptVariables(ClassDeclarationSyntax classNode, SemanticModel semanticModel, out List<ScriptVariableSyntax> variables)
{
variables = [];
var fields = classNode.Members.OfType<FieldDeclarationSyntax>();
foreach (var field in fields)
{
Type resolvedType = semanticModel.ToSystemType(field.Declaration.Type) ?? throw new Exception($"Failed to resolve type for field: {field.Declaration.Type.ToFullString()}");
// Check if the field has VariableAttribute
VariableAttribute? varAttr = null;
foreach (var attrList in field.AttributeLists)
{
foreach (var attr in attrList.Attributes)
{
var attrType = semanticModel.GetTypeAttribute(attr);
if (attrType == VariableAttributeType)
{
varAttr = semanticModel.GetConstantAttribute(attr, attrType) as RobotNet.Script.VariableAttribute;
break;
}
}
if (varAttr != null) break;
}
foreach (var variable in field.Declaration.Variables)
{
var name = variable.Identifier.Text;
if (string.IsNullOrEmpty(name)) continue;
if (variable.Initializer is null)
{
var value = resolvedType.IsValueType ? Activator.CreateInstance(resolvedType) : null;
variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false));
}
else
{
var constant = semanticModel.GetConstantValue(variable.Initializer.Value);
if (constant.HasValue)
{
try
{
var value = Convert.ChangeType(constant.Value, resolvedType);
variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false));
}
catch (Exception ex)
{
throw new Exception($"Failed to convert value of {name} = \"{constant.Value}\" to {resolvedType}: {ex}");
}
}
else
{
var code = variable.Initializer.Value.ToFullString();
object? value;
if (string.IsNullOrEmpty(code))
{
value = resolvedType.IsValueType ? Activator.CreateInstance(resolvedType) : null;
}
else
{
value = CSharpScript.EvaluateAsync<object>(code, ScriptConfiguration.ScriptOptions).GetAwaiter().GetResult();
}
variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false));
}
}
}
}
// TODO: kiểm tra các properties là auto-property có đủ và getter và setter thì add vào variables
var properties = classNode.Members.OfType<PropertyDeclarationSyntax>();
foreach (var prop in properties)
{
// Kiểm tra có cả getter và setter
var accessors = prop.AccessorList?.Accessors.ToList() ?? [];
bool hasGetter = accessors?.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration) == true;
bool hasSetter = accessors?.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration) == true;
// Kiểm tra auto-property: cả getter và setter đều không có body và không phải expression-bodied
bool isAutoProperty = hasGetter && hasSetter &&
accessors!.All(a => a.Body == null && a.ExpressionBody == null) &&
prop.ExpressionBody == null;
if (isAutoProperty)
{
var name = prop.Identifier.Text;
var type = semanticModel.ToSystemType(prop.Type) ?? throw new Exception($"Failed to resolve type for property: {prop.Type.ToFullString()}");
// Giá trị mặc định của auto-property là default(T)
object? value = type.IsValueType ? Activator.CreateInstance(type) : null;
variables.Add(new ScriptVariableSyntax(name, type, value, false, false));
}
}
}
private static void GetScriptTasksAndMissions(ClassDeclarationSyntax classNode, SemanticModel semanticModel, out List<ScriptTaskData> tasks, out List<ScriptMissionData> missions)
{
tasks = [];
missions = [];
var methods = classNode.Members.OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
bool attrDone = false;
foreach (var attrList in method.AttributeLists)
{
foreach (var attr in attrList.Attributes)
{
var attrType = semanticModel.GetTypeAttribute(attr);
if (attrType == TaskAttributeType)
{
attrDone = true;
if (semanticModel.GetConstantAttribute(attr, attrType) is not RobotNet.Script.TaskAttribute taskAttr)
{
throw new Exception($"Failed to get TaskAttribute from method {method.Identifier.Text}");
}
// Check if method returns Task or Task<T>
var returnType = semanticModel.ToSystemType(method.ReturnType);
bool isTask = returnType == typeof(System.Threading.Tasks.Task) ||
(returnType != null && returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(System.Threading.Tasks.Task<>));
bool isVoid = returnType == typeof(void);
if (method.ParameterList.Parameters.Count > 0 || !(isTask || isVoid))
{
throw new Exception($"Task Method {method.Identifier.Text} with TaskAttribute must have no parameters and return type void or Task.");
}
tasks.Add(new ScriptTaskData(method.Identifier.Text,
taskAttr.Interval,
taskAttr.AutoStart,
method.ToFullString(),
ExtractRelatedCodeForScriptRunner(classNode, method, $"{(isTask ? "await " : "")}{method.Identifier.Text}();")));
break;
}
else if (attrType == MisionAttributeType)
{
attrDone = true;
if (semanticModel.GetConstantAttribute(attr, attrType) is not RobotNet.Script.MissionAttribute missionAttr)
{
throw new Exception($"Failed to get MissionAttribute from method {method.Identifier.Text}");
}
var returnType = semanticModel.ToSystemType(method.ReturnType);
if (returnType is null || returnType != MisionReturnType)
{
throw new Exception($"Mission Method {method.Identifier.Text} with MissionAttribute must return type IEnumerator<MissionState>.");
}
var inputParameters = new List<string>();
var parameters = new List<ScriptMissionParameter>();
bool hasCancellationTokenParameter = false;
foreach (var param in method.ParameterList.Parameters)
{
if (param.Type is null)
{
throw new Exception($"Parameter {param.Identifier.Text} in method {method.Identifier.Text} has no type specified.");
}
var paramType = semanticModel.ToSystemType(param.Type) ?? throw new Exception($"Failed to resolve type for parameter {param.Identifier.Text} in method {method.Identifier.Text}");
if (!MissionParameterTypes.Contains(paramType))
{
throw new Exception($"Parameter type {param.Type} {param.Identifier.Text} in method mission {method.Identifier.Text} is not supported");
}
if (paramType == typeof(CancellationToken))
{
if (hasCancellationTokenParameter)
{
throw new Exception($"Method {method.Identifier.Text} has multiple CancellationToken parameters, which is not allowed.");
}
hasCancellationTokenParameter = true;
}
// lấy default value nếu có
object? defaultValue = null;
if (param.Default is EqualsValueClauseSyntax equalsValue)
{
var constValue = semanticModel.GetConstantValue(equalsValue.Value);
if (constValue.HasValue)
{
defaultValue = constValue.Value;
}
}
//inputParameters.Add($@"({paramType.FullName})parameters["""+param.Identifier.Text+"""]");
inputParameters.Add($@"({paramType.FullName})parameters[""{param.Identifier.Text}""]");
parameters.Add(new ScriptMissionParameter(param.Identifier.Text, paramType, defaultValue));
}
var execScript = $"return {method.Identifier.Text}({string.Join(", ", inputParameters)});";
missions.Add(new ScriptMissionData(method.Identifier.Text,
parameters,
method.ToFullString(),
ExtractRelatedCodeForScriptRunner(classNode, method, execScript),
missionAttr.IsMultipleRun));
break;
}
}
if (attrDone) break;
}
}
}
private static string ExtractRelatedCodeForScriptRunner(ClassDeclarationSyntax classNode, MethodDeclarationSyntax rootMethod, string execScript)
{
var allMethods = classNode.Members.OfType<MethodDeclarationSyntax>().ToList();
var allFields = classNode.Members.OfType<FieldDeclarationSyntax>().ToList();
var allProperties = classNode.Members.OfType<PropertyDeclarationSyntax>().ToList();
var allNestedTypes = classNode.Members
.Where(m => m is ClassDeclarationSyntax || m is StructDeclarationSyntax || m is InterfaceDeclarationSyntax || m is EnumDeclarationSyntax)
.ToList();
// 1. BFS: method + non-auto-property
var usedMethodNames = new HashSet<string>();
var usedPropertyNames = new HashSet<string>();
var methodQueue = new Queue<MemberDeclarationSyntax>();
var collectedMethods = new List<MethodDeclarationSyntax>();
var collectedNonAutoProperties = new List<PropertyDeclarationSyntax>();
methodQueue.Enqueue(rootMethod);
while (methodQueue.Count > 0)
{
var member = methodQueue.Dequeue();
if (member is MethodDeclarationSyntax method)
{
if (!usedMethodNames.Add(method.Identifier.Text))
continue;
collectedMethods.Add(method);
// Tìm các method/property được gọi trong method này
var invokedNames = method.DescendantNodes()
.OfType<IdentifierNameSyntax>()
.Select(id => id.Identifier.Text)
.Distinct();
foreach (var name in invokedNames)
{
// Method
var nextMethod = allMethods.FirstOrDefault(m => m.Identifier.Text == name);
if (nextMethod != null && !usedMethodNames.Contains(name))
methodQueue.Enqueue(nextMethod);
// Property
var nextProp = allProperties.FirstOrDefault(p => p.Identifier.Text == name);
if (nextProp != null && !usedPropertyNames.Contains(name))
methodQueue.Enqueue(nextProp);
}
}
else if (member is PropertyDeclarationSyntax prop)
{
if (!usedPropertyNames.Add(prop.Identifier.Text))
continue;
// Auto-property: bỏ qua, sẽ xử lý sau
var accessors = prop.AccessorList?.Accessors.ToList() ?? [];
bool hasGetter = accessors.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration);
bool hasSetter = accessors.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration);
bool isAutoProperty = hasGetter && hasSetter &&
accessors.All(a => a.Body == null && a.ExpressionBody == null) &&
prop.ExpressionBody == null;
if (isAutoProperty)
continue;
collectedNonAutoProperties.Add(prop);
// Tìm các method/property/field được gọi trong property này
var invokedNames = prop.DescendantNodes()
.OfType<IdentifierNameSyntax>()
.Select(id => id.Identifier.Text)
.Distinct();
foreach (var name in invokedNames)
{
// Method
var nextMethod = allMethods.FirstOrDefault(m => m.Identifier.Text == name);
if (nextMethod != null && !usedMethodNames.Contains(name))
methodQueue.Enqueue(nextMethod);
// Property
var nextProp = allProperties.FirstOrDefault(p => p.Identifier.Text == name);
if (nextProp != null && !usedPropertyNames.Contains(name))
methodQueue.Enqueue(nextProp);
}
}
}
// 2. Collect all used member names (from all collected methods & non-auto-properties)
var usedMemberNames = new HashSet<string>();
foreach (var method in collectedMethods)
{
foreach (var id in method.DescendantNodes().OfType<IdentifierNameSyntax>().Select(id => id.Identifier.Text))
usedMemberNames.Add(id);
}
foreach (var prop in collectedNonAutoProperties)
{
foreach (var id in prop.DescendantNodes().OfType<IdentifierNameSyntax>().Select(id => id.Identifier.Text))
usedMemberNames.Add(id);
}
// 3. Collect fields
var relatedFields = new List<string>();
foreach (var field in allFields)
{
foreach (var variable in field.Declaration.Variables)
{
if (usedMemberNames.Contains(variable.Identifier.Text))
{
var varType = field.Declaration.Type.ToString();
var varName = variable.Identifier.Text;
var propertyCode = $@"public {varType} {varName}
{{
get => ({varType})globals[""{varName}""];
set => globals[""{varName}""] = value;
}}";
relatedFields.Add(propertyCode.Trim());
}
}
}
// 4. Collect auto-properties
var relatedAutoProperties = new List<string>();
foreach (var prop in allProperties)
{
var accessors = prop.AccessorList?.Accessors.ToList() ?? [];
bool hasGetter = accessors.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration);
bool hasSetter = accessors.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration);
bool isAutoProperty = hasGetter && hasSetter &&
accessors.All(a => a.Body == null && a.ExpressionBody == null) &&
prop.ExpressionBody == null;
if (isAutoProperty && (usedMemberNames.Contains(prop.Identifier.Text) || usedPropertyNames.Contains(prop.Identifier.Text)))
{
var propType = prop.Type.ToString();
var propName = prop.Identifier.Text;
var propertyCode = $@"public {propType} {propName}
{{
get => ({propType})globals[""{propName}""];
set => globals[""{propName}""] = value;
}}";
relatedAutoProperties.Add(propertyCode.Trim());
}
}
// 5. Collect nested types if referenced
var usedNestedTypeNames = new HashSet<string>(usedMemberNames);
var relatedNestedTypes = allNestedTypes
.Where(nt =>
{
if (nt is BaseTypeDeclarationSyntax btd)
return usedNestedTypeNames.Contains(btd.Identifier.Text);
return false;
})
.Select(nt => nt.NormalizeWhitespace().ToFullString())
.ToList();
// 6. Compose the script
var sb = new StringBuilder();
sb.AppendLine(ScriptConfiguration.RuntimeGlobalsScript);
foreach (var nt in relatedNestedTypes)
sb.AppendLine(nt);
foreach (var f in relatedFields)
sb.AppendLine(f);
foreach (var p in relatedAutoProperties)
sb.AppendLine(p);
foreach (var p in collectedNonAutoProperties)
sb.AppendLine(p.NormalizeWhitespace().ToFullString());
foreach (var m in collectedMethods)
sb.AppendLine(m.NormalizeWhitespace().ToFullString());
sb.AppendLine(execScript);
return sb.ToString();
}
private static Type? GetTypeAttribute(this SemanticModel semanticModel, AttributeSyntax attrSynctax)
{
var typeInfo = semanticModel.GetTypeInfo(attrSynctax);
if (typeInfo.Type is null) return null;
string metadataName = typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
return ResolveTypeFromString(metadataName);
}
private static object? GetConstantAttribute(this SemanticModel semanticModel, AttributeSyntax attrSynctax, Type type)
{
var args = new List<object?>();
foreach (var arg in attrSynctax.ArgumentList?.Arguments ?? default)
{
var constValue = semanticModel.GetConstantValue(arg.Expression);
if (constValue.HasValue)
args.Add(constValue.Value);
else
args.Add(null); // fallback nếu không phân giải được
}
// Find the constructor with the same number or more parameters (with optional)
var ctors = type.GetConstructors();
foreach (var ctor in ctors)
{
var parameters = ctor.GetParameters();
if (args.Count <= parameters.Length)
{
// Fill missing optional parameters with their default values
var finalArgs = args.ToList();
for (int i = args.Count; i < parameters.Length; i++)
{
if (parameters[i].IsOptional)
finalArgs.Add(parameters[i].DefaultValue);
else
goto NextCtor; // Not enough arguments and not optional
}
return ctor.Invoke([.. finalArgs]);
}
NextCtor:;
}
return null;
}
private static Type? ToSystemType(this SemanticModel semanticModel, TypeSyntax typeSyntax)
{
var typeSymbol = semanticModel.GetTypeInfo(typeSyntax).Type;
if (typeSymbol is null) return null;
if (typeSyntax is PredefinedTypeSyntax predefinedType
&& predefinedMap.TryGetValue(predefinedType.Keyword.Text, out var systemType))
{
return systemType;
}
string metadataName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
return metadataName.Equals("void") ? typeof(void) : ResolveTypeFromString(metadataName);
}
private static Type? ResolveTypeFromString(string typeString)
{
typeString = typeString.Trim();
// Handle array types (e.g., System.Int32[], string[], List<int>[])
if (typeString.EndsWith("[]"))
{
var elementTypeString = typeString[..^2].Trim();
var elementType = ResolveTypeFromString(elementTypeString);
return elementType?.MakeArrayType();
}
// Trường hợp không phải generic
if (!typeString.Contains('<'))
{
return FindType(typeString);
}
// Tách phần generic
var match = Regex.Match(typeString, @"^(?<raw>[^<]+)<(?<args>.+)>$");
if (!match.Success)
return null;
var genericTypeName = match.Groups["raw"].Value;
var genericArgsString = match.Groups["args"].Value;
// Phân tách các generic argument (xử lý nested generics)
var genericArgs = SplitGenericArguments(genericArgsString);
var genericType = FindType(genericTypeName + "`" + genericArgs.Count);
if (genericType == null)
return null;
var resolvedArgs = genericArgs.Select(ResolveTypeFromString).ToArray();
if (resolvedArgs.Any(t => t == null))
return null;
return genericType.MakeGenericType(resolvedArgs!);
}
private static List<string> SplitGenericArguments(string input)
{
var args = new List<string>();
var sb = new StringBuilder();
int depth = 0;
foreach (char c in input)
{
if (c == ',' && depth == 0)
{
args.Add(sb.ToString().Trim());
sb.Clear();
}
else
{
if (c == '<') depth++;
else if (c == '>') depth--;
sb.Append(c);
}
}
if (sb.Length > 0)
{
args.Add(sb.ToString().Trim());
}
return args;
}
private static Type? FindType(string typeName)
{
if (predefinedMap.TryGetValue(typeName, out var systemType)) return systemType;
return AppDomain.CurrentDomain
.GetAssemblies()
.Select(a => a.GetType(typeName, false))
.FirstOrDefault(t => t != null);
}
public static string ToTypeString(Type type)
{
if (type == typeof(void))
return "void";
if (type.IsGenericType)
{
var genericTypeName = type.GetGenericTypeDefinition().FullName;
if (genericTypeName == null)
return type.Name;
var backtickIndex = genericTypeName.IndexOf('`');
if (backtickIndex > 0)
genericTypeName = genericTypeName.Substring(0, backtickIndex);
var genericArgs = type.GetGenericArguments();
var argsString = string.Join(", ", genericArgs.Select(ToTypeString));
return $"{genericTypeName}<{argsString}>";
}
return type.FullName ?? type.Name;
}
private static readonly Dictionary<string, Type> predefinedMap = new()
{
["bool"] = typeof(bool),
["byte"] = typeof(byte),
["sbyte"] = typeof(sbyte),
["short"] = typeof(short),
["ushort"] = typeof(ushort),
["int"] = typeof(int),
["uint"] = typeof(uint),
["long"] = typeof(long),
["ulong"] = typeof(ulong),
["float"] = typeof(float),
["double"] = typeof(double),
["decimal"] = typeof(decimal),
["char"] = typeof(char),
["string"] = typeof(string),
["object"] = typeof(object)
};
private static readonly Type TaskAttributeType = typeof(RobotNet.Script.TaskAttribute);
private static readonly Type MisionAttributeType = typeof(RobotNet.Script.MissionAttribute);
private static readonly Type MisionReturnType = typeof(IAsyncEnumerable<MissionState>);
private static readonly Type VariableAttributeType = typeof(VariableAttribute);
private static readonly Type RobotType = typeof(RobotNet.Script.IRobot);
private static readonly ImmutableArray<Type> MissionParameterTypes = [
typeof(bool),
typeof(byte),
typeof(sbyte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(double),
typeof(decimal),
typeof(char),
typeof(string),
typeof(CancellationToken),
];
}