first commit -push
This commit is contained in:
707
RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs
Normal file
707
RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs
Normal file
@@ -0,0 +1,707 @@
|
||||
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),
|
||||
];
|
||||
}
|
||||
226
RobotNet.ScriptManager/Helpers/ScriptConfiguration.cs
Normal file
226
RobotNet.ScriptManager/Helpers/ScriptConfiguration.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using RobotNet.Script.Shares;
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace RobotNet.ScriptManager.Helpers;
|
||||
|
||||
public static class ScriptConfiguration
|
||||
{
|
||||
public static readonly string ScriptStorePath;
|
||||
public static readonly string DllsPath;
|
||||
public static readonly ScriptOptions ScriptOptions;
|
||||
public static readonly ImmutableArray<MetadataReference> MetadataReferences;
|
||||
public static readonly ImmutableArray<string> UsingNamespaces;
|
||||
public static readonly string UsingNamespacesScript;
|
||||
public static readonly string DeveloptGlobalsScript;
|
||||
public static readonly string RuntimeGlobalsScript;
|
||||
|
||||
static ScriptConfiguration()
|
||||
{
|
||||
ScriptStorePath = "scripts";
|
||||
DllsPath = "dlls";
|
||||
UsingNamespaces = ["System", "System.Collections.Generic", "System.Linq.Expressions", "System.Threading", "System.Threading.Tasks", "System.Runtime.CompilerServices", "RobotNet.Script"];
|
||||
UsingNamespacesScript = string.Join(Environment.NewLine, UsingNamespaces.Select(ns => $"using {ns};"));
|
||||
|
||||
List<MetadataReference> metadataRefs = [];
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
if (Directory.Exists(DllsPath))
|
||||
{
|
||||
foreach (var dll in Directory.GetFiles(DllsPath, "*.dll"))
|
||||
{
|
||||
metadataRefs.Add(MetadataReference.CreateFromFile(Path.Combine(currentDirectory, dll), properties: MetadataReferenceProperties.Assembly));
|
||||
}
|
||||
}
|
||||
|
||||
MetadataReferences = [.. metadataRefs];
|
||||
var options = ScriptOptions.Default;
|
||||
options.MetadataReferences.Clear();
|
||||
ScriptOptions = options.AddReferences(MetadataReferences).AddImports(UsingNamespaces).WithEmitDebugInformation(false);
|
||||
|
||||
DeveloptGlobalsScript = BuildDeveloptGlobalsScript();
|
||||
RuntimeGlobalsScript = BuildRuntimeGlobalsScript();
|
||||
}
|
||||
|
||||
public static string GetScriptCode() => ReadAllTextFromFolder(ScriptStorePath);
|
||||
|
||||
private static string ReadAllTextFromFolder(string path)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(path);
|
||||
|
||||
var code = string.Join(Environment.NewLine, [.. dirInfo.GetFiles("*.cs").Select(f => File.ReadAllText(f.FullName))]);
|
||||
var after = string.Join(Environment.NewLine, dirInfo.GetDirectories().Select(dir => ReadAllTextFromFolder(dir.FullName)).ToArray());
|
||||
return string.Join(Environment.NewLine, code, after);
|
||||
}
|
||||
|
||||
private static string BuildRuntimeGlobalsScript()
|
||||
{
|
||||
var type = typeof(IRobotNetGlobals);
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
sb.AppendLine($@"{CSharpSyntaxHelper.ToTypeString(field.FieldType)} {field.Name}
|
||||
{{
|
||||
get => ({CSharpSyntaxHelper.ToTypeString(field.FieldType)})robotnet[""{field.Name}""];
|
||||
set => robotnet[""{field.Name}""] = value;
|
||||
}}");
|
||||
}
|
||||
|
||||
foreach (var prop in type.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($"{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)} {prop.Name}");
|
||||
propBuilder.AppendLine("{");
|
||||
if (hasGetter)
|
||||
propBuilder.AppendLine($@" get => ((Func<{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)}>)robotnet[""get_{prop.Name}""])();");
|
||||
|
||||
if (hasSetter)
|
||||
propBuilder.AppendLine($@" set => ((Action<{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)}>)robotnet[""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 => $"{CSharpSyntaxHelper.ToTypeString(p.ParameterType)} {p.Name}"));
|
||||
var paramNames = string.Join(", ", indexParams.Select(p => p.Name));
|
||||
var getterDelegateType = $"Func<{string.Join(", ", indexParams.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(prop.PropertyType)]))}>";
|
||||
var setterDelegateType = $"Action<{string.Join(", ", indexParams.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(prop.PropertyType)]))}>";
|
||||
|
||||
var propBuilder = new StringBuilder();
|
||||
propBuilder.AppendLine($"{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)} this[{paramDecl}]");
|
||||
propBuilder.AppendLine("{");
|
||||
if (prop.GetGetMethod() != null)
|
||||
propBuilder.AppendLine($@" get => (({getterDelegateType})robotnet[""get_{prop.Name}_indexer""])({paramNames});");
|
||||
if (prop.GetSetMethod() != null)
|
||||
propBuilder.AppendLine($@" set => (({setterDelegateType})robotnet[""set_{prop.Name}_indexer""])({(string.IsNullOrEmpty(paramNames) ? "value" : paramNames + ", value")});");
|
||||
propBuilder.AppendLine("}");
|
||||
sb.AppendLine(propBuilder.ToString());
|
||||
}
|
||||
}
|
||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (!method.IsSpecialName)
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
var parametersStr = string.Join(", ", parameters.Select(ToParameterString));
|
||||
var args = string.Join(", ", parameters.Select(p => p.Name));
|
||||
var returnType = CSharpSyntaxHelper.ToTypeString(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 => CSharpSyntaxHelper.ToTypeString(p.ParameterType)))}>")
|
||||
: $"Func<{string.Join(", ", parameters.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(method.ReturnType)]))}>";
|
||||
|
||||
sb.AppendLine($@"{returnType} {method.Name}({parametersStr}) => (({methodType})robotnet[""{methodKey}""]){(string.IsNullOrEmpty(args) ? "()" : $"({args})")};");
|
||||
}
|
||||
}
|
||||
|
||||
var code = sb.ToString();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string BuildDeveloptGlobalsScript()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var glovalType = typeof(IRobotNetGlobals);
|
||||
|
||||
var properties = glovalType.GetProperties();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propStr = "";
|
||||
if (property.CanRead)
|
||||
{
|
||||
propStr = $"{CSharpSyntaxHelper.ToTypeString(property.PropertyType)} {property.Name} {{ get; {(property.CanWrite ? "set; " : "")}}}";
|
||||
}
|
||||
else if (property.CanWrite)
|
||||
{
|
||||
propStr = $"{CSharpSyntaxHelper.ToTypeString(property.PropertyType)} {property.Name} {{ set => throw new System.NotImplementedException(); }}";
|
||||
}
|
||||
if(string.IsNullOrEmpty(propStr)) continue;
|
||||
sb.AppendLine(propStr);
|
||||
}
|
||||
|
||||
var fields = glovalType.GetFields();
|
||||
foreach (var field in fields)
|
||||
{
|
||||
sb.AppendLine($"{CSharpSyntaxHelper.ToTypeString(field.FieldType)} {field.Name};");
|
||||
}
|
||||
|
||||
var methods = glovalType.GetMethods();
|
||||
foreach (var method in methods)
|
||||
{
|
||||
if (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")) continue;
|
||||
|
||||
var notImplementedMethod = $"{CSharpSyntaxHelper.ToTypeString(method.ReturnType)} {method.Name}({string.Join(',', method.GetParameters().Select(ToParameterString))}) => throw new System.NotImplementedException();";
|
||||
sb.AppendLine(notImplementedMethod);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ToParameterString(ParameterInfo parameter)
|
||||
{
|
||||
var modifier = "";
|
||||
if (parameter.IsDefined(typeof(ParamArrayAttribute), false))
|
||||
modifier = "params ";
|
||||
else if (parameter.IsIn && parameter.ParameterType.IsByRef && !parameter.IsOut)
|
||||
modifier = "in ";
|
||||
else if (parameter.IsOut)
|
||||
modifier = "out ";
|
||||
else if (parameter.ParameterType.IsByRef)
|
||||
modifier = "ref ";
|
||||
|
||||
var typeString = CSharpSyntaxHelper.ToTypeString(
|
||||
parameter.ParameterType.IsByRef
|
||||
? parameter.ParameterType.GetElementType()!
|
||||
: parameter.ParameterType
|
||||
);
|
||||
|
||||
var defaultValue = "";
|
||||
if (parameter.HasDefaultValue)
|
||||
{
|
||||
if (parameter.DefaultValue != null)
|
||||
{
|
||||
if (parameter.ParameterType.IsEnum)
|
||||
{
|
||||
defaultValue = $" = {CSharpSyntaxHelper.ToTypeString(parameter.ParameterType)}.{parameter.DefaultValue}";
|
||||
}
|
||||
else if (parameter.DefaultValue is string)
|
||||
{
|
||||
defaultValue = $" = \"{parameter.DefaultValue}\"";
|
||||
}
|
||||
else if (parameter.DefaultValue is bool b)
|
||||
{
|
||||
defaultValue = $" = {b.ToString().ToLower()}";
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultValue = $" = {parameter.DefaultValue}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultValue = " = null";
|
||||
}
|
||||
}
|
||||
|
||||
return $"{modifier}{typeString} {parameter.Name}{defaultValue}";
|
||||
}
|
||||
|
||||
}
|
||||
74
RobotNet.ScriptManager/Helpers/VDA5050Helper.cs
Normal file
74
RobotNet.ScriptManager/Helpers/VDA5050Helper.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using RobotNet.RobotShares.Models;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Expressions;
|
||||
|
||||
namespace RobotNet.ScriptManager.Helpers;
|
||||
|
||||
public static class VDA5050ScriptHelper
|
||||
{
|
||||
public static RobotState ConvertToRobotState(RobotStateModel model)
|
||||
{
|
||||
bool isReady = model.IsOnline && !model.OrderState.IsProcessing && model.Errors.Length == 0;
|
||||
if(model.ActionStates.Length > 0)
|
||||
{
|
||||
isReady = isReady && model.ActionStates.All(a => !a.IsProcessing);
|
||||
}
|
||||
return new RobotState(isReady, model.BatteryState.BatteryVoltage, model.Loads.Length != 0, model.BatteryState.Charging, model.AgvPosition.X, model.AgvPosition.Y, model.AgvPosition.Theta);
|
||||
}
|
||||
|
||||
public static RobotInstantActionModel ConvertToRobotInstantActionModel(string robotId, RobotAction action)
|
||||
{
|
||||
return new RobotInstantActionModel
|
||||
{
|
||||
RobotId = robotId,
|
||||
Action = new RobotShares.VDA5050.InstantAction.Action
|
||||
{
|
||||
ActionType = action.ActionType,
|
||||
BlockingType = action.BlockingType switch
|
||||
{
|
||||
BlockingType.NONE => "NONE",
|
||||
BlockingType.SOFT => "SOFT",
|
||||
BlockingType.HARD => "HARD",
|
||||
_ => "NONE"
|
||||
},
|
||||
ActionParameters = [..action.Parameters?.Select(p => new RobotShares.VDA5050.InstantAction.ActionParameter
|
||||
{
|
||||
Key = p.Key,
|
||||
Value = p.Value
|
||||
}) ?? []],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static RobotShares.Models.RobotMoveToNodeModel ConvertToRobotMoveToNodeModel(string robotId, string nodeName, IDictionary<string, IEnumerable<RobotAction>> actions, double? lastAngle)
|
||||
{
|
||||
return new RobotShares.Models.RobotMoveToNodeModel
|
||||
{
|
||||
RobotId = robotId,
|
||||
NodeName = nodeName,
|
||||
LastAngle = lastAngle ?? 0,
|
||||
OverrideLastAngle = lastAngle != null,
|
||||
Actions = actions.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.Select(a => new RobotShares.VDA5050.InstantAction.Action
|
||||
{
|
||||
ActionType = a.ActionType,
|
||||
ActionId = Guid.NewGuid().ToString(),
|
||||
BlockingType = a.BlockingType switch
|
||||
{
|
||||
BlockingType.NONE => "NONE",
|
||||
BlockingType.SOFT => "SOFT",
|
||||
BlockingType.HARD => "HARD",
|
||||
_ => "NONE"
|
||||
},
|
||||
ActionParameters = [..a.Parameters?.Select(p => new RobotShares.VDA5050.InstantAction.ActionParameter
|
||||
{
|
||||
Key = p.Key,
|
||||
Value = p.Value
|
||||
}) ?? []],
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user