Initial commit
This commit is contained in:
@@ -0,0 +1,704 @@
|
||||
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<string, object?> ConvertGlobalsToDictionary(object globals, Type type)
|
||||
{
|
||||
var globalsDic = new Dictionary<string, object?>();
|
||||
|
||||
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<ScriptVariableModel> ExportScriptVariables(ClassDeclarationSyntax classNode, SemanticModel semanticModel, ScriptOptions scriptOptions)
|
||||
{
|
||||
List<ScriptVariableModel> variables = [];
|
||||
var fields = classNode.Members.OfType<FieldDeclarationSyntax>();
|
||||
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<object>(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<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);
|
||||
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<ScriptTaskModel> tasks,
|
||||
out List<ScriptMissionModel> 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 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<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)
|
||||
{
|
||||
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<MissionStatus>. Found return type: {returnType.Name}.");
|
||||
}
|
||||
|
||||
var inputParameters = new List<string>();
|
||||
var parameters = new List<ScriptMissionParameterModel>();
|
||||
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<IAsyncEnumerable<MissionStatus>>(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<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
|
||||
// Sử dụng InvocationExpressionSyntax và MemberAccessExpressionSyntax để tìm chính xác hơn
|
||||
var invocationExpressions = method.DescendantNodes()
|
||||
.OfType<InvocationExpressionSyntax>()
|
||||
.Select(inv => inv.Expression)
|
||||
.OfType<MemberAccessExpressionSyntax>()
|
||||
.Select(ma => ma.Name.Identifier.Text)
|
||||
.Distinct();
|
||||
|
||||
var memberAccessExpressions = method.DescendantNodes()
|
||||
.OfType<MemberAccessExpressionSyntax>()
|
||||
.Select(ma => ma.Name.Identifier.Text)
|
||||
.Distinct();
|
||||
|
||||
var identifierNames = method.DescendantNodes()
|
||||
.OfType<IdentifierNameSyntax>()
|
||||
.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<InvocationExpressionSyntax>()
|
||||
.Select(inv => inv.Expression)
|
||||
.OfType<MemberAccessExpressionSyntax>()
|
||||
.Select(ma => ma.Name.Identifier.Text)
|
||||
.Distinct();
|
||||
|
||||
var memberAccessExpressions = prop.DescendantNodes()
|
||||
.OfType<MemberAccessExpressionSyntax>()
|
||||
.Select(ma => ma.Name.Identifier.Text)
|
||||
.Distinct();
|
||||
|
||||
var identifierNames = prop.DescendantNodes()
|
||||
.OfType<IdentifierNameSyntax>()
|
||||
.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<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}){nameof(ScriptGlobals.GlobalVariables)}[""{varName}""];
|
||||
set => {nameof(ScriptGlobals.GlobalVariables)}[""{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}){nameof(ScriptGlobals.GlobalVariables)}[""{propName}""];
|
||||
set => {nameof(ScriptGlobals.GlobalVariables)}[""{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(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<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
|
||||
&& 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<RobotNet10.Script.MissionStatus>);
|
||||
private static readonly Type VariableAttributeType = typeof(RobotNet10.Script.VariableAttribute);
|
||||
}
|
||||
Reference in New Issue
Block a user