ScriptSignalsGenerator.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Text;
  4. using Microsoft.CodeAnalysis;
  5. using Microsoft.CodeAnalysis.CSharp.Syntax;
  6. using Microsoft.CodeAnalysis.Text;
  7. // TODO:
  8. // Determine a proper way to emit the signal.
  9. // 'Emit(nameof(TheEvent))' creates a StringName every time and has the overhead of string marshaling.
  10. // I haven't decided on the best option yet. Some possibilities:
  11. // - Expose the generated StringName fields to the user, for use with 'Emit(...)'.
  12. // - Generate a 'EmitSignalName' method for each event signal.
  13. namespace Godot.SourceGenerators
  14. {
  15. [Generator]
  16. public class ScriptSignalsGenerator : ISourceGenerator
  17. {
  18. public void Initialize(GeneratorInitializationContext context)
  19. {
  20. }
  21. public void Execute(GeneratorExecutionContext context)
  22. {
  23. if (context.IsGodotSourceGeneratorDisabled("ScriptSignals"))
  24. return;
  25. INamedTypeSymbol[] godotClasses = context
  26. .Compilation.SyntaxTrees
  27. .SelectMany(tree =>
  28. tree.GetRoot().DescendantNodes()
  29. .OfType<ClassDeclarationSyntax>()
  30. .SelectGodotScriptClasses(context.Compilation)
  31. // Report and skip non-partial classes
  32. .Where(x =>
  33. {
  34. if (x.cds.IsPartial())
  35. {
  36. if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
  37. {
  38. Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
  39. return false;
  40. }
  41. return true;
  42. }
  43. Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
  44. return false;
  45. })
  46. .Select(x => x.symbol)
  47. )
  48. .Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
  49. .ToArray();
  50. if (godotClasses.Length > 0)
  51. {
  52. var typeCache = new MarshalUtils.TypeCache(context.Compilation);
  53. foreach (var godotClass in godotClasses)
  54. {
  55. VisitGodotScriptClass(context, typeCache, godotClass);
  56. }
  57. }
  58. }
  59. internal static string SignalDelegateSuffix = "EventHandler";
  60. private static void VisitGodotScriptClass(
  61. GeneratorExecutionContext context,
  62. MarshalUtils.TypeCache typeCache,
  63. INamedTypeSymbol symbol
  64. )
  65. {
  66. INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
  67. string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
  68. namespaceSymbol.FullQualifiedNameOmitGlobal() :
  69. string.Empty;
  70. bool hasNamespace = classNs.Length != 0;
  71. bool isInnerClass = symbol.ContainingType != null;
  72. string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
  73. + "_ScriptSignals.generated";
  74. var source = new StringBuilder();
  75. source.Append("using Godot;\n");
  76. source.Append("using Godot.NativeInterop;\n");
  77. source.Append("\n");
  78. if (hasNamespace)
  79. {
  80. source.Append("namespace ");
  81. source.Append(classNs);
  82. source.Append(" {\n\n");
  83. }
  84. if (isInnerClass)
  85. {
  86. var containingType = symbol.ContainingType;
  87. while (containingType != null)
  88. {
  89. source.Append("partial ");
  90. source.Append(containingType.GetDeclarationKeyword());
  91. source.Append(" ");
  92. source.Append(containingType.NameWithTypeParameters());
  93. source.Append("\n{\n");
  94. containingType = containingType.ContainingType;
  95. }
  96. }
  97. source.Append("partial class ");
  98. source.Append(symbol.NameWithTypeParameters());
  99. source.Append("\n{\n");
  100. var members = symbol.GetMembers();
  101. var signalDelegateSymbols = members
  102. .Where(s => s.Kind == SymbolKind.NamedType)
  103. .Cast<INamedTypeSymbol>()
  104. .Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate)
  105. .Where(s => s.GetAttributes()
  106. .Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false));
  107. List<GodotSignalDelegateData> godotSignalDelegates = new();
  108. foreach (var signalDelegateSymbol in signalDelegateSymbols)
  109. {
  110. if (!signalDelegateSymbol.Name.EndsWith(SignalDelegateSuffix))
  111. {
  112. Common.ReportSignalDelegateMissingSuffix(context, signalDelegateSymbol);
  113. continue;
  114. }
  115. string signalName = signalDelegateSymbol.Name;
  116. signalName = signalName.Substring(0, signalName.Length - SignalDelegateSuffix.Length);
  117. var invokeMethodData = signalDelegateSymbol
  118. .DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache);
  119. if (invokeMethodData == null)
  120. {
  121. if (signalDelegateSymbol.DelegateInvokeMethod is IMethodSymbol methodSymbol)
  122. {
  123. foreach (var parameter in methodSymbol.Parameters)
  124. {
  125. if (parameter.RefKind != RefKind.None)
  126. {
  127. Common.ReportSignalParameterTypeNotSupported(context, parameter);
  128. continue;
  129. }
  130. var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(parameter.Type, typeCache);
  131. if (marshalType == null)
  132. {
  133. Common.ReportSignalParameterTypeNotSupported(context, parameter);
  134. }
  135. }
  136. if (!methodSymbol.ReturnsVoid)
  137. {
  138. Common.ReportSignalDelegateSignatureMustReturnVoid(context, signalDelegateSymbol);
  139. }
  140. }
  141. continue;
  142. }
  143. godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value));
  144. }
  145. source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");
  146. source.Append(" /// <summary>\n")
  147. .Append(" /// Cached StringNames for the signals contained in this class, for fast lookup.\n")
  148. .Append(" /// </summary>\n");
  149. source.Append(
  150. $" public new class SignalName : {symbol.BaseType.FullQualifiedNameIncludeGlobal()}.SignalName {{\n");
  151. // Generate cached StringNames for methods and properties, for fast lookup
  152. foreach (var signalDelegate in godotSignalDelegates)
  153. {
  154. string signalName = signalDelegate.Name;
  155. source.Append(" /// <summary>\n")
  156. .Append(" /// Cached name for the '")
  157. .Append(signalName)
  158. .Append("' signal.\n")
  159. .Append(" /// </summary>\n");
  160. source.Append(" public new static readonly global::Godot.StringName ");
  161. source.Append(signalName);
  162. source.Append(" = \"");
  163. source.Append(signalName);
  164. source.Append("\";\n");
  165. }
  166. source.Append(" }\n"); // class GodotInternal
  167. // Generate GetGodotSignalList
  168. if (godotSignalDelegates.Count > 0)
  169. {
  170. const string listType = "global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>";
  171. source.Append(" /// <summary>\n")
  172. .Append(" /// Get the signal information for all the signals declared in this class.\n")
  173. .Append(" /// This method is used by Godot to register the available signals in the editor.\n")
  174. .Append(" /// Do not call this method.\n")
  175. .Append(" /// </summary>\n");
  176. source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
  177. source.Append(" internal new static ")
  178. .Append(listType)
  179. .Append(" GetGodotSignalList()\n {\n");
  180. source.Append(" var signals = new ")
  181. .Append(listType)
  182. .Append("(")
  183. .Append(godotSignalDelegates.Count)
  184. .Append(");\n");
  185. foreach (var signalDelegateData in godotSignalDelegates)
  186. {
  187. var methodInfo = DetermineMethodInfo(signalDelegateData);
  188. AppendMethodInfo(source, methodInfo);
  189. }
  190. source.Append(" return signals;\n");
  191. source.Append(" }\n");
  192. }
  193. source.Append("#pragma warning restore CS0109\n");
  194. // Generate signal event
  195. foreach (var signalDelegate in godotSignalDelegates)
  196. {
  197. string signalName = signalDelegate.Name;
  198. // TODO: Hide backing event from code-completion and debugger
  199. // The reason we have a backing field is to hide the invoke method from the event,
  200. // as it doesn't emit the signal, only the event delegates. This can confuse users.
  201. // Maybe we should directly connect the delegates, as we do with native signals?
  202. source.Append(" private ")
  203. .Append(signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal())
  204. .Append(" backing_")
  205. .Append(signalName)
  206. .Append(";\n");
  207. source.Append(
  208. $" /// <inheritdoc cref=\"{signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal()}\"/>\n");
  209. source.Append(" public event ")
  210. .Append(signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal())
  211. .Append(" ")
  212. .Append(signalName)
  213. .Append(" {\n")
  214. .Append(" add => backing_")
  215. .Append(signalName)
  216. .Append(" += value;\n")
  217. .Append(" remove => backing_")
  218. .Append(signalName)
  219. .Append(" -= value;\n")
  220. .Append("}\n");
  221. }
  222. // Generate RaiseGodotClassSignalCallbacks
  223. if (godotSignalDelegates.Count > 0)
  224. {
  225. source.Append(" /// <inheritdoc/>\n");
  226. source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
  227. source.Append(
  228. " protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, ");
  229. source.Append("NativeVariantPtrArgs args)\n {\n");
  230. foreach (var signal in godotSignalDelegates)
  231. {
  232. GenerateSignalEventInvoker(signal, source);
  233. }
  234. source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args);\n");
  235. source.Append(" }\n");
  236. }
  237. // Generate HasGodotClassSignal
  238. if (godotSignalDelegates.Count > 0)
  239. {
  240. source.Append(" /// <inheritdoc/>\n");
  241. source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
  242. source.Append(
  243. " protected override bool HasGodotClassSignal(in godot_string_name signal)\n {\n");
  244. bool isFirstEntry = true;
  245. foreach (var signal in godotSignalDelegates)
  246. {
  247. GenerateHasSignalEntry(signal.Name, source, isFirstEntry);
  248. isFirstEntry = false;
  249. }
  250. source.Append(" return base.HasGodotClassSignal(signal);\n");
  251. source.Append(" }\n");
  252. }
  253. source.Append("}\n"); // partial class
  254. if (isInnerClass)
  255. {
  256. var containingType = symbol.ContainingType;
  257. while (containingType != null)
  258. {
  259. source.Append("}\n"); // outer class
  260. containingType = containingType.ContainingType;
  261. }
  262. }
  263. if (hasNamespace)
  264. {
  265. source.Append("\n}\n");
  266. }
  267. context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
  268. }
  269. private static void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo)
  270. {
  271. source.Append(" signals.Add(new(name: SignalName.")
  272. .Append(methodInfo.Name)
  273. .Append(", returnVal: ");
  274. AppendPropertyInfo(source, methodInfo.ReturnVal);
  275. source.Append(", flags: (global::Godot.MethodFlags)")
  276. .Append((int)methodInfo.Flags)
  277. .Append(", arguments: ");
  278. if (methodInfo.Arguments is { Count: > 0 })
  279. {
  280. source.Append("new() { ");
  281. foreach (var param in methodInfo.Arguments)
  282. {
  283. AppendPropertyInfo(source, param);
  284. // C# allows colon after the last element
  285. source.Append(", ");
  286. }
  287. source.Append(" }");
  288. }
  289. else
  290. {
  291. source.Append("null");
  292. }
  293. source.Append(", defaultArguments: null));\n");
  294. }
  295. private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo)
  296. {
  297. source.Append("new(type: (global::Godot.Variant.Type)")
  298. .Append((int)propertyInfo.Type)
  299. .Append(", name: \"")
  300. .Append(propertyInfo.Name)
  301. .Append("\", hint: (global::Godot.PropertyHint)")
  302. .Append((int)propertyInfo.Hint)
  303. .Append(", hintString: \"")
  304. .Append(propertyInfo.HintString)
  305. .Append("\", usage: (global::Godot.PropertyUsageFlags)")
  306. .Append((int)propertyInfo.Usage)
  307. .Append(", exported: ")
  308. .Append(propertyInfo.Exported ? "true" : "false");
  309. if (propertyInfo.ClassName != null)
  310. {
  311. source.Append(", className: new global::Godot.StringName(\"")
  312. .Append(propertyInfo.ClassName)
  313. .Append("\")");
  314. }
  315. source.Append(")");
  316. }
  317. private static MethodInfo DetermineMethodInfo(GodotSignalDelegateData signalDelegateData)
  318. {
  319. var invokeMethodData = signalDelegateData.InvokeMethodData;
  320. PropertyInfo returnVal;
  321. if (invokeMethodData.RetType != null)
  322. {
  323. returnVal = DeterminePropertyInfo(invokeMethodData.RetType.Value.MarshalType,
  324. invokeMethodData.RetType.Value.TypeSymbol,
  325. name: string.Empty);
  326. }
  327. else
  328. {
  329. returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None,
  330. hintString: null, PropertyUsageFlags.Default, exported: false);
  331. }
  332. int paramCount = invokeMethodData.ParamTypes.Length;
  333. List<PropertyInfo>? arguments;
  334. if (paramCount > 0)
  335. {
  336. arguments = new(capacity: paramCount);
  337. for (int i = 0; i < paramCount; i++)
  338. {
  339. arguments.Add(DeterminePropertyInfo(invokeMethodData.ParamTypes[i],
  340. invokeMethodData.Method.Parameters[i].Type,
  341. name: invokeMethodData.Method.Parameters[i].Name));
  342. }
  343. }
  344. else
  345. {
  346. arguments = null;
  347. }
  348. return new MethodInfo(signalDelegateData.Name, returnVal, MethodFlags.Default, arguments,
  349. defaultArguments: null);
  350. }
  351. private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, ITypeSymbol typeSymbol, string name)
  352. {
  353. var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
  354. var propUsage = PropertyUsageFlags.Default;
  355. if (memberVariantType == VariantType.Nil)
  356. propUsage |= PropertyUsageFlags.NilIsVariant;
  357. string? className = null;
  358. if (memberVariantType == VariantType.Object && typeSymbol is INamedTypeSymbol namedTypeSymbol)
  359. {
  360. className = namedTypeSymbol.GetGodotScriptNativeClassName();
  361. }
  362. return new PropertyInfo(memberVariantType, name,
  363. PropertyHint.None, string.Empty, propUsage, className, exported: false);
  364. }
  365. private static void GenerateHasSignalEntry(
  366. string signalName,
  367. StringBuilder source,
  368. bool isFirstEntry
  369. )
  370. {
  371. source.Append(" ");
  372. if (!isFirstEntry)
  373. source.Append("else ");
  374. source.Append("if (signal == SignalName.");
  375. source.Append(signalName);
  376. source.Append(") {\n return true;\n }\n");
  377. }
  378. private static void GenerateSignalEventInvoker(
  379. GodotSignalDelegateData signal,
  380. StringBuilder source
  381. )
  382. {
  383. string signalName = signal.Name;
  384. var invokeMethodData = signal.InvokeMethodData;
  385. source.Append(" if (signal == SignalName.");
  386. source.Append(signalName);
  387. source.Append(" && args.Count == ");
  388. source.Append(invokeMethodData.ParamTypes.Length);
  389. source.Append(") {\n");
  390. source.Append(" backing_");
  391. source.Append(signalName);
  392. source.Append("?.Invoke(");
  393. for (int i = 0; i < invokeMethodData.ParamTypes.Length; i++)
  394. {
  395. if (i != 0)
  396. source.Append(", ");
  397. source.AppendNativeVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"),
  398. invokeMethodData.ParamTypeSymbols[i], invokeMethodData.ParamTypes[i]);
  399. }
  400. source.Append(");\n");
  401. source.Append(" return;\n");
  402. source.Append(" }\n");
  403. }
  404. }
  405. }