using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Scripting; using RobotNet10.Script; using RobotNet10.ScriptEngine.Models; using RobotNet10.ScriptEngine.Shared; using System.Linq.Expressions; using System.Reflection; using System.Text; namespace RobotNet10.ScriptEngine.Helpers; public static class ScriptHelper { public static Dictionary ConvertGlobalsToDictionary(object globals, Type type) { var globalsDic = new Dictionary(); foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) { globalsDic.TryAdd(field.Name, field.GetValue(globals)); } foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (prop.GetIndexParameters().Length == 0) { // Forward getter if available if (prop.GetGetMethod() != null) { var getter = Delegate.CreateDelegate( Expression.GetDelegateType([prop.PropertyType]), globals, prop.GetGetMethod()! ); globalsDic.TryAdd($"get_{prop.Name}", getter); } // Forward setter if available if (prop.GetSetMethod() != null) { var setter = Delegate.CreateDelegate( Expression.GetDelegateType([prop.PropertyType, typeof(void)]), globals, prop.GetSetMethod()! ); globalsDic.TryAdd($"set_{prop.Name}", setter); } } else { // Handle indexers (properties with parameters) var indexParams = prop.GetIndexParameters().Select(p => p.ParameterType).ToArray(); // Forward indexer getter if available if (prop.GetGetMethod() != null) { var getterParamTypes = indexParams.Concat([prop.PropertyType]).ToArray(); var getterDelegate = Delegate.CreateDelegate( Expression.GetDelegateType(getterParamTypes), globals, prop.GetGetMethod()! ); globalsDic.TryAdd($"get_{prop.Name}_indexer", getterDelegate); } // Forward indexer setter if available if (prop.GetSetMethod() != null) { var setterParamTypes = indexParams.Concat([prop.PropertyType, typeof(void)]).ToArray(); var setterDelegate = Delegate.CreateDelegate( Expression.GetDelegateType(setterParamTypes), globals, prop.GetSetMethod()! ); globalsDic.TryAdd($"set_{prop.Name}_indexer", setterDelegate); } } } foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) { if (!method.IsSpecialName) { var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.FullName} {p.Name}")); var args = string.Join(", ", method.GetParameters().Select(p => p.Name)); var returnType = method.ReturnType == typeof(void) ? "void" : method.ReturnType.FullName; var paramTypes = string.Join(",", method.GetParameters().Select(p => p.ParameterType.FullName)); var methodKey = $"{method.Name}({paramTypes})"; var del = Delegate.CreateDelegate( Expression.GetDelegateType([.. method.GetParameters().Select(p => p.ParameterType), method.ReturnType]), globals, method ); globalsDic.TryAdd(methodKey, del); } } return globalsDic; } public static string BuildGlobalsScript(Type globalType) { var sb = new StringBuilder(); sb.AppendLine(BuildGlobalsScript(typeof(IScriptGlobals), nameof(ScriptGlobals.RobotNet))); sb.AppendLine(BuildGlobalsScript(globalType, nameof(ScriptGlobals.AppApis))); return sb.ToString(); } public static string BuildGlobalsScript(Type globalType, string nameOfGlobals) { var sb = new StringBuilder(); foreach (var field in globalType.GetFields(BindingFlags.Public | BindingFlags.Instance)) { sb.AppendLine($@"{ScriptHelpers.ToString(field.FieldType)} {field.Name} {{ get => ({ScriptHelpers.ToString(field.FieldType)}){nameOfGlobals}[""{field.Name}""]; set => {nameOfGlobals}[""{field.Name}""] = value; }}"); } foreach (var prop in globalType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (prop.GetIndexParameters().Length == 0) { var hasGetter = prop.GetGetMethod() != null; var hasSetter = prop.GetSetMethod() != null; if (hasGetter || hasSetter) { var propBuilder = new StringBuilder(); propBuilder.AppendLine($"{ScriptHelpers.ToString(prop.PropertyType)} {prop.Name}"); propBuilder.AppendLine("{"); if (hasGetter) propBuilder.AppendLine($@" get => ((Func<{ScriptHelpers.ToString(prop.PropertyType)}>){nameOfGlobals}[""get_{prop.Name}""])();"); if (hasSetter) propBuilder.AppendLine($@" set => ((Action<{ScriptHelpers.ToString(prop.PropertyType)}>){nameOfGlobals}[""set_{prop.Name}""])(value);"); propBuilder.AppendLine("}"); sb.AppendLine(propBuilder.ToString()); } } else { // Handle indexers (properties with parameters) var indexParams = prop.GetIndexParameters(); var paramDecl = string.Join(", ", indexParams.Select(p => $"{ScriptHelpers.ToString(p.ParameterType)} {p.Name}")); var paramNames = string.Join(", ", indexParams.Select(p => p.Name)); var getterDelegateType = $"Func<{string.Join(", ", indexParams.Select(p => ScriptHelpers.ToString(p.ParameterType)).Concat([ScriptHelpers.ToString(prop.PropertyType)]))}>"; var setterDelegateType = $"Action<{string.Join(", ", indexParams.Select(p => ScriptHelpers.ToString(p.ParameterType)).Concat([ScriptHelpers.ToString(prop.PropertyType)]))}>"; var propBuilder = new StringBuilder(); propBuilder.AppendLine($"{ScriptHelpers.ToString(prop.PropertyType)} this[{paramDecl}]"); propBuilder.AppendLine("{"); if (prop.GetGetMethod() != null) propBuilder.AppendLine($@" get => (({getterDelegateType}){nameOfGlobals}[""get_{prop.Name}_indexer""])({paramNames});"); if (prop.GetSetMethod() != null) propBuilder.AppendLine($@" set => (({setterDelegateType}){nameOfGlobals}[""set_{prop.Name}_indexer""])({(string.IsNullOrEmpty(paramNames) ? "value" : paramNames + ", value")});"); propBuilder.AppendLine("}"); sb.AppendLine(propBuilder.ToString()); } } foreach (var method in globalType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) { if (!method.IsSpecialName) { var parameters = method.GetParameters() ?? []; var parametersStr = string.Join(", ", parameters.Select(ScriptHelpers.ToString)); var args = string.Join(", ", parameters.Select(p => p.Name)); var returnType = ScriptHelpers.ToString(method.ReturnType); var paramTypes = string.Join(",", parameters.Select(p => p.ParameterType.FullName)); var methodKey = $"{method.Name}({paramTypes})"; var methodType = method.ReturnType == typeof(void) ? (parameters.Length == 0 ? "Action" : $"Action<{string.Join(", ", parameters.Select(p => ScriptHelpers.ToString(p.ParameterType)))}>") : $"Func<{string.Join(", ", parameters.Select(p => ScriptHelpers.ToString(p.ParameterType)).Concat([ScriptHelpers.ToString(method.ReturnType)]))}>"; sb.AppendLine($@"{returnType} {method.Name}({parametersStr}) => (({methodType}){nameOfGlobals}[""{methodKey}""]){(string.IsNullOrEmpty(args) ? "()" : $"({args})")};"); } } return sb.ToString(); } public static IEnumerable ExportScriptVariables(ClassDeclarationSyntax classNode, SemanticModel semanticModel, ScriptOptions scriptOptions) { List variables = []; var fields = classNode.Members.OfType(); foreach (var field in fields) { Type? resolvedType = semanticModel.ToSystemType(field.Declaration.Type); if (resolvedType == null) { var fieldName = field.Declaration.Variables.FirstOrDefault()?.Identifier.Text ?? "unknown"; throw new ScriptCompilationException($"Failed to resolve type for field '{fieldName}': {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 RobotNet10.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 ScriptVariableModel(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 ScriptVariableModel(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false)); } catch (Exception ex) { throw new ScriptCompilationException($"Failed to convert value of variable '{name}' = \"{constant.Value}\" to type '{resolvedType.Name}': {ex.Message}", 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, scriptOptions).GetAwaiter().GetResult(); } variables.Add(new ScriptVariableModel(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false)); } } } } // Check auto-properties (properties with both getter and setter, no body) and add them to 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); if (type == null) { throw new ScriptCompilationException($"Failed to resolve type for auto-property '{name}': {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 ScriptVariableModel(name, type, value, false, false)); } } return variables; } public static void ExportScriptTasksAndMissions(ClassDeclarationSyntax classNode, SemanticModel semanticModel, string globalScript, ScriptOptions scriptOptions, 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 RobotNet10.Script.TaskAttribute taskAttr) { throw new ScriptCompilationException($"Failed to get TaskAttribute from method '{method.Identifier.Text}'. Ensure the attribute has valid parameters."); } // 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) { throw new ScriptCompilationException($"Task method '{method.Identifier.Text}' with [Task] attribute must have no parameters. Found {method.ParameterList.Parameters.Count} parameter(s)."); } if (!(isTask || isVoid)) { var actualReturnType = returnType?.Name ?? "unknown"; throw new ScriptCompilationException($"Task method '{method.Identifier.Text}' with [Task] attribute must return void or Task. Found return type: {actualReturnType}."); } var code = ExtractRelatedCodeForScriptRunner(classNode, method, globalScript, $"{(isTask ? "await " : "")}{method.Identifier.Text}();"); var script = CSharpScript.Create(code, scriptOptions, globalsType: typeof(ScriptGlobals)); tasks.Add(new ScriptTaskModel(method.Identifier.Text, taskAttr.Interval, taskAttr.AutoStart, method.ToFullString(), script.CreateDelegate())); break; } else if (attrType == MissionAttributeType) { attrDone = true; if (semanticModel.GetConstantAttribute(attr, attrType) is not RobotNet10.Script.MissionAttribute missionAttr) { throw new ScriptCompilationException($"Failed to get MissionAttribute from method '{method.Identifier.Text}'. Ensure the attribute has valid parameters."); } var returnType = semanticModel.ToSystemType(method.ReturnType); if (returnType is null) { throw new ScriptCompilationException($"Failed to resolve return type for mission method '{method.Identifier.Text}'."); } if (returnType != MissionReturnType) { throw new ScriptCompilationException($"Mission method '{method.Identifier.Text}' with [Mission] attribute must return IAsyncEnumerable. Found return type: {returnType.Name}."); } 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 ScriptCompilationException($"Parameter '{param.Identifier.Text}' in mission method '{method.Identifier.Text}' has no type specified."); } var paramType = semanticModel.ToSystemType(param.Type); if (paramType == null) { throw new ScriptCompilationException($"Failed to resolve type for parameter '{param.Identifier.Text}' in mission method '{method.Identifier.Text}'. Type: {param.Type.ToFullString()}"); } if (paramType == typeof(CancellationToken)) { if (hasCancellationTokenParameter) { throw new ScriptCompilationException($"Mission method '{method.Identifier.Text}' has multiple CancellationToken parameters, which is not allowed. Only one CancellationToken parameter is supported."); } hasCancellationTokenParameter = true; } else if (!ScriptHelpers.SupportedTypes.Values.Contains(paramType)) { throw new ScriptCompilationException($"Parameter type '{paramType.Name}' for parameter '{param.Identifier.Text}' in mission method '{method.Identifier.Text}' is not supported. Supported types: {string.Join(", ", ScriptHelpers.SupportedTypes.Values.Select(t => t.Name))}"); } // 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}){nameof(ScriptGlobals.MissionParameters)}[""{param.Identifier.Text}""]"); parameters.Add(new ScriptMissionParameterModel(param.Identifier.Text, paramType, defaultValue)); } var execScript = $"return {method.Identifier.Text}({string.Join(", ", inputParameters)});"; var code = ExtractRelatedCodeForScriptRunner(classNode, method, globalScript, execScript); var script = CSharpScript.Create>(code, scriptOptions, globalsType: typeof(ScriptGlobals)); missions.Add(new ScriptMissionModel(method.Identifier.Text, parameters, method.ToFullString(), missionAttr.TotalScore, missionAttr.IsMultipleRun, missionAttr.AutoStart, script.CreateDelegate())); break; } } if (attrDone) break; } } } private static string ExtractRelatedCodeForScriptRunner(ClassDeclarationSyntax classNode, MethodDeclarationSyntax rootMethod, string globalScript, 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 // Sử dụng InvocationExpressionSyntax và MemberAccessExpressionSyntax để tìm chính xác hơn var invocationExpressions = method.DescendantNodes() .OfType() .Select(inv => inv.Expression) .OfType() .Select(ma => ma.Name.Identifier.Text) .Distinct(); var memberAccessExpressions = method.DescendantNodes() .OfType() .Select(ma => ma.Name.Identifier.Text) .Distinct(); var identifierNames = method.DescendantNodes() .OfType() .Where(id => id.Parent is not MemberAccessExpressionSyntax) // Exclude already handled member accesses .Select(id => id.Identifier.Text) .Distinct(); var allInvokedNames = invocationExpressions .Concat(memberAccessExpressions) .Concat(identifierNames) .Distinct(); foreach (var name in allInvokedNames) { // 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 // Sử dụng InvocationExpressionSyntax và MemberAccessExpressionSyntax để tìm chính xác hơn var invocationExpressions = prop.DescendantNodes() .OfType() .Select(inv => inv.Expression) .OfType() .Select(ma => ma.Name.Identifier.Text) .Distinct(); var memberAccessExpressions = prop.DescendantNodes() .OfType() .Select(ma => ma.Name.Identifier.Text) .Distinct(); var identifierNames = prop.DescendantNodes() .OfType() .Where(id => id.Parent is not MemberAccessExpressionSyntax) // Exclude already handled member accesses .Select(id => id.Identifier.Text) .Distinct(); var allInvokedNames = invocationExpressions .Concat(memberAccessExpressions) .Concat(identifierNames) .Distinct(); foreach (var name in allInvokedNames) { // 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}){nameof(ScriptGlobals.GlobalVariables)}[""{varName}""]; set => {nameof(ScriptGlobals.GlobalVariables)}[""{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}){nameof(ScriptGlobals.GlobalVariables)}[""{propName}""]; set => {nameof(ScriptGlobals.GlobalVariables)}[""{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(globalScript); 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 ScriptHelpers.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 && ScriptHelpers.SupportedTypes.TryGetValue(predefinedType.Keyword.Text, out var systemType)) { return systemType; } string metadataName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); return metadataName.Equals("void") ? typeof(void) : ScriptHelpers.ResolveTypeFromString(metadataName); } private static readonly Type TaskAttributeType = typeof(RobotNet10.Script.TaskAttribute); private static readonly Type MissionAttributeType = typeof(RobotNet10.Script.MissionAttribute); private static readonly Type MissionReturnType = typeof(IAsyncEnumerable); private static readonly Type VariableAttributeType = typeof(RobotNet10.Script.VariableAttribute); }