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 variables, out List tasks, out List missions) { try { var listVariables = new List(); 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().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 variables) { variables = []; var fields = classNode.Members.OfType(); 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(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(); 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 tasks, out List missions) { tasks = []; missions = []; var methods = classNode.Members.OfType(); 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 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."); } var inputParameters = new List(); var parameters = new List(); 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().ToList(); var allFields = classNode.Members.OfType().ToList(); var allProperties = classNode.Members.OfType().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(); var usedPropertyNames = new HashSet(); var methodQueue = new Queue(); var collectedMethods = new List(); var collectedNonAutoProperties = new List(); 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() .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() .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(); foreach (var method in collectedMethods) { foreach (var id in method.DescendantNodes().OfType().Select(id => id.Identifier.Text)) usedMemberNames.Add(id); } foreach (var prop in collectedNonAutoProperties) { foreach (var id in prop.DescendantNodes().OfType().Select(id => id.Identifier.Text)) usedMemberNames.Add(id); } // 3. Collect fields var relatedFields = new List(); 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(); 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(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(); 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[]) 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, @"^(?[^<]+)<(?.+)>$"); 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 SplitGenericArguments(string input) { var args = new List(); 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 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); private static readonly Type VariableAttributeType = typeof(VariableAttribute); private static readonly Type RobotType = typeof(RobotNet.Script.IRobot); private static readonly ImmutableArray 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), ]; }