ScriptBoilerplateGenerator.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. using System.Collections.Generic;
  2. using System.Collections.Immutable;
  3. using System.Linq;
  4. using System.Text;
  5. using Microsoft.CodeAnalysis;
  6. using Microsoft.CodeAnalysis.CSharp.Syntax;
  7. using Microsoft.CodeAnalysis.Text;
  8. namespace Godot.SourceGenerators
  9. {
  10. [Generator]
  11. public class ScriptBoilerplateGenerator : ISourceGenerator
  12. {
  13. public void Execute(GeneratorExecutionContext context)
  14. {
  15. if (context.AreGodotSourceGeneratorsDisabled())
  16. return;
  17. INamedTypeSymbol[] godotClasses = context
  18. .Compilation.SyntaxTrees
  19. .SelectMany(tree =>
  20. tree.GetRoot().DescendantNodes()
  21. .OfType<ClassDeclarationSyntax>()
  22. .SelectGodotScriptClasses(context.Compilation)
  23. // Report and skip non-partial classes
  24. .Where(x =>
  25. {
  26. if (x.cds.IsPartial())
  27. {
  28. if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
  29. {
  30. Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
  31. return false;
  32. }
  33. return true;
  34. }
  35. Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
  36. return false;
  37. })
  38. .Select(x => x.symbol)
  39. )
  40. .Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
  41. .ToArray();
  42. if (godotClasses.Length > 0)
  43. {
  44. var typeCache = new MarshalUtils.TypeCache(context);
  45. foreach (var godotClass in godotClasses)
  46. {
  47. VisitGodotScriptClass(context, typeCache, godotClass);
  48. }
  49. }
  50. }
  51. private static void VisitGodotScriptClass(
  52. GeneratorExecutionContext context,
  53. MarshalUtils.TypeCache typeCache,
  54. INamedTypeSymbol symbol
  55. )
  56. {
  57. string className = symbol.Name;
  58. INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
  59. string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
  60. namespaceSymbol.FullQualifiedName() :
  61. string.Empty;
  62. bool hasNamespace = classNs.Length != 0;
  63. bool isInnerClass = symbol.ContainingType != null;
  64. string uniqueName = hasNamespace ?
  65. classNs + "." + className + "_ScriptBoilerplate_Generated" :
  66. className + "_ScriptBoilerplate_Generated";
  67. var source = new StringBuilder();
  68. source.Append("using Godot;\n");
  69. source.Append("using Godot.NativeInterop;\n");
  70. source.Append("\n");
  71. if (hasNamespace)
  72. {
  73. source.Append("namespace ");
  74. source.Append(classNs);
  75. source.Append(" {\n\n");
  76. }
  77. if (isInnerClass)
  78. {
  79. var containingType = symbol.ContainingType;
  80. while (containingType != null)
  81. {
  82. source.Append("partial ");
  83. source.Append(containingType.GetDeclarationKeyword());
  84. source.Append(" ");
  85. source.Append(containingType.Name);
  86. source.Append("\n{\n");
  87. containingType = containingType.ContainingType;
  88. }
  89. }
  90. source.Append("partial class ");
  91. source.Append(className);
  92. source.Append("\n{\n");
  93. var members = symbol.GetMembers();
  94. // TODO: Static static marshaling (no reflection, no runtime type checks)
  95. var methodSymbols = members
  96. .Where(s => s.Kind == SymbolKind.Method)
  97. .Cast<IMethodSymbol>()
  98. .Where(m => m.MethodKind == MethodKind.Ordinary && !m.IsImplicitlyDeclared);
  99. var propertySymbols = members
  100. .Where(s => s.Kind == SymbolKind.Property)
  101. .Cast<IPropertySymbol>();
  102. var fieldSymbols = members
  103. .Where(s => s.Kind == SymbolKind.Field)
  104. .Cast<IFieldSymbol>()
  105. .Where(p => !p.IsImplicitlyDeclared);
  106. var godotClassMethods = WhereHasCompatibleGodotType(methodSymbols, typeCache).ToArray();
  107. var godotClassProperties = WhereIsCompatibleGodotType(propertySymbols, typeCache).ToArray();
  108. var godotClassFields = WhereIsCompatibleGodotType(fieldSymbols, typeCache).ToArray();
  109. source.Append(" private class GodotInternal {\n");
  110. // Generate cached StringNames for methods and properties, for fast lookup
  111. foreach (var method in godotClassMethods)
  112. {
  113. string methodName = method.Method.Name;
  114. source.Append(" public static readonly StringName MethodName_");
  115. source.Append(methodName);
  116. source.Append(" = \"");
  117. source.Append(methodName);
  118. source.Append("\";\n");
  119. }
  120. foreach (var property in godotClassProperties)
  121. {
  122. string propertyName = property.Property.Name;
  123. source.Append(" public static readonly StringName PropName_");
  124. source.Append(propertyName);
  125. source.Append(" = \"");
  126. source.Append(propertyName);
  127. source.Append("\";\n");
  128. }
  129. foreach (var field in godotClassFields)
  130. {
  131. string fieldName = field.Field.Name;
  132. source.Append(" public static readonly StringName PropName_");
  133. source.Append(fieldName);
  134. source.Append(" = \"");
  135. source.Append(fieldName);
  136. source.Append("\";\n");
  137. }
  138. source.Append(" }\n"); // class GodotInternal
  139. // Generate InvokeGodotClassMethod
  140. if (godotClassMethods.Length > 0)
  141. {
  142. source.Append(" protected override bool InvokeGodotClassMethod(in godot_string_name method, ");
  143. source.Append("NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n {\n");
  144. foreach (var method in godotClassMethods)
  145. {
  146. GenerateMethodInvoker(method, source);
  147. }
  148. source.Append(" return base.InvokeGodotClassMethod(method, args, argCount, out ret);\n");
  149. source.Append(" }\n");
  150. }
  151. // Generate Set/GetGodotClassPropertyValue
  152. if (godotClassProperties.Length > 0 || godotClassFields.Length > 0)
  153. {
  154. bool isFirstEntry;
  155. // Setters
  156. bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.Field.IsReadOnly) &&
  157. godotClassProperties.All(pi => pi.Property.IsReadOnly);
  158. if (!allPropertiesAreReadOnly)
  159. {
  160. source.Append(" protected override bool SetGodotClassPropertyValue(in godot_string_name name, ");
  161. source.Append("in godot_variant value)\n {\n");
  162. isFirstEntry = true;
  163. foreach (var property in godotClassProperties)
  164. {
  165. if (property.Property.IsReadOnly)
  166. continue;
  167. GeneratePropertySetter(property.Property.Name,
  168. property.Property.Type.FullQualifiedName(), source, isFirstEntry);
  169. isFirstEntry = false;
  170. }
  171. foreach (var field in godotClassFields)
  172. {
  173. if (field.Field.IsReadOnly)
  174. continue;
  175. GeneratePropertySetter(field.Field.Name,
  176. field.Field.Type.FullQualifiedName(), source, isFirstEntry);
  177. isFirstEntry = false;
  178. }
  179. source.Append(" return base.SetGodotClassPropertyValue(name, value);\n");
  180. source.Append(" }\n");
  181. }
  182. // Getters
  183. source.Append(" protected override bool GetGodotClassPropertyValue(in godot_string_name name, ");
  184. source.Append("out godot_variant value)\n {\n");
  185. isFirstEntry = true;
  186. foreach (var property in godotClassProperties)
  187. {
  188. GeneratePropertyGetter(property.Property.Name, source, isFirstEntry);
  189. isFirstEntry = false;
  190. }
  191. foreach (var field in godotClassFields)
  192. {
  193. GeneratePropertyGetter(field.Field.Name, source, isFirstEntry);
  194. isFirstEntry = false;
  195. }
  196. source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n");
  197. source.Append(" }\n");
  198. }
  199. // Generate HasGodotClassMethod
  200. if (godotClassMethods.Length > 0)
  201. {
  202. source.Append(" protected override bool HasGodotClassMethod(in godot_string_name method)\n {\n");
  203. bool isFirstEntry = true;
  204. foreach (var method in godotClassMethods)
  205. {
  206. GenerateHasMethodEntry(method, source, isFirstEntry);
  207. isFirstEntry = false;
  208. }
  209. source.Append(" return base.HasGodotClassMethod(method);\n");
  210. source.Append(" }\n");
  211. }
  212. source.Append("}\n"); // partial class
  213. if (isInnerClass)
  214. {
  215. var containingType = symbol.ContainingType;
  216. while (containingType != null)
  217. {
  218. source.Append("}\n"); // outer class
  219. containingType = containingType.ContainingType;
  220. }
  221. }
  222. if (hasNamespace)
  223. {
  224. source.Append("\n}\n");
  225. }
  226. context.AddSource(uniqueName, SourceText.From(source.ToString(), Encoding.UTF8));
  227. }
  228. private static void GenerateMethodInvoker(
  229. GodotMethodInfo method,
  230. StringBuilder source
  231. )
  232. {
  233. string methodName = method.Method.Name;
  234. source.Append(" if (method == GodotInternal.MethodName_");
  235. source.Append(methodName);
  236. source.Append(" && argCount == ");
  237. source.Append(method.ParamTypes.Length);
  238. source.Append(") {\n");
  239. if (method.RetType != null)
  240. source.Append(" object retBoxed = ");
  241. else
  242. source.Append(" ");
  243. source.Append(methodName);
  244. source.Append("(");
  245. for (int i = 0; i < method.ParamTypes.Length; i++)
  246. {
  247. if (i != 0)
  248. source.Append(", ");
  249. // TODO: static marshaling (no reflection, no runtime type checks)
  250. string paramTypeQualifiedName = method.ParamTypeSymbols[i].FullQualifiedName();
  251. source.Append("(");
  252. source.Append(paramTypeQualifiedName);
  253. source.Append(")Marshaling.ConvertVariantToManagedObjectOfType(args[");
  254. source.Append(i);
  255. source.Append("], typeof(");
  256. source.Append(paramTypeQualifiedName);
  257. source.Append("))");
  258. }
  259. source.Append(");\n");
  260. if (method.RetType != null)
  261. {
  262. // TODO: static marshaling (no reflection, no runtime type checks)
  263. source.Append(" ret = Marshaling.ConvertManagedObjectToVariant(retBoxed);\n");
  264. source.Append(" return true;\n");
  265. }
  266. else
  267. {
  268. source.Append(" ret = default;\n");
  269. source.Append(" return true;\n");
  270. }
  271. source.Append(" }\n");
  272. }
  273. private static void GeneratePropertySetter(
  274. string propertyMemberName,
  275. string propertyTypeQualifiedName,
  276. StringBuilder source,
  277. bool isFirstEntry
  278. )
  279. {
  280. source.Append(" ");
  281. if (!isFirstEntry)
  282. source.Append("else ");
  283. source.Append("if (name == GodotInternal.PropName_");
  284. source.Append(propertyMemberName);
  285. source.Append(") {\n");
  286. source.Append(" ");
  287. source.Append(propertyMemberName);
  288. source.Append(" = ");
  289. // TODO: static marshaling (no reflection, no runtime type checks)
  290. source.Append("(");
  291. source.Append(propertyTypeQualifiedName);
  292. source.Append(")Marshaling.ConvertVariantToManagedObjectOfType(value, typeof(");
  293. source.Append(propertyTypeQualifiedName);
  294. source.Append("));\n");
  295. source.Append(" return true;\n");
  296. source.Append(" }\n");
  297. }
  298. private static void GeneratePropertyGetter(
  299. string propertyMemberName,
  300. StringBuilder source,
  301. bool isFirstEntry
  302. )
  303. {
  304. source.Append(" ");
  305. if (!isFirstEntry)
  306. source.Append("else ");
  307. source.Append("if (name == GodotInternal.PropName_");
  308. source.Append(propertyMemberName);
  309. source.Append(") {\n");
  310. // TODO: static marshaling (no reflection, no runtime type checks)
  311. source.Append(" value = Marshaling.ConvertManagedObjectToVariant(");
  312. source.Append(propertyMemberName);
  313. source.Append(");\n");
  314. source.Append(" return true;\n");
  315. source.Append(" }\n");
  316. }
  317. private static void GenerateHasMethodEntry(
  318. GodotMethodInfo method,
  319. StringBuilder source,
  320. bool isFirstEntry
  321. )
  322. {
  323. string methodName = method.Method.Name;
  324. source.Append(" ");
  325. if (!isFirstEntry)
  326. source.Append("else ");
  327. source.Append("if (method == GodotInternal.MethodName_");
  328. source.Append(methodName);
  329. source.Append(") {\n return true;\n }\n");
  330. }
  331. public void Initialize(GeneratorInitializationContext context)
  332. {
  333. }
  334. private struct GodotMethodInfo
  335. {
  336. public GodotMethodInfo(IMethodSymbol method, ImmutableArray<MarshalType> paramTypes,
  337. ImmutableArray<ITypeSymbol> paramTypeSymbols, MarshalType? retType)
  338. {
  339. Method = method;
  340. ParamTypes = paramTypes;
  341. ParamTypeSymbols = paramTypeSymbols;
  342. RetType = retType;
  343. }
  344. public IMethodSymbol Method { get; }
  345. public ImmutableArray<MarshalType> ParamTypes { get; }
  346. public ImmutableArray<ITypeSymbol> ParamTypeSymbols { get; }
  347. public MarshalType? RetType { get; }
  348. }
  349. private struct GodotPropertyInfo
  350. {
  351. public GodotPropertyInfo(IPropertySymbol property, MarshalType type)
  352. {
  353. Property = property;
  354. Type = type;
  355. }
  356. public IPropertySymbol Property { get; }
  357. public MarshalType Type { get; }
  358. }
  359. private struct GodotFieldInfo
  360. {
  361. public GodotFieldInfo(IFieldSymbol field, MarshalType type)
  362. {
  363. Field = field;
  364. Type = type;
  365. }
  366. public IFieldSymbol Field { get; }
  367. public MarshalType Type { get; }
  368. }
  369. private static IEnumerable<GodotMethodInfo> WhereHasCompatibleGodotType(
  370. IEnumerable<IMethodSymbol> methods,
  371. MarshalUtils.TypeCache typeCache
  372. )
  373. {
  374. foreach (var method in methods)
  375. {
  376. if (method.IsGenericMethod)
  377. continue;
  378. var retType = method.ReturnsVoid ?
  379. null :
  380. MarshalUtils.ConvertManagedTypeToVariantType(method.ReturnType, typeCache);
  381. if (retType == null && !method.ReturnsVoid)
  382. continue;
  383. var parameters = method.Parameters;
  384. var paramTypes = parameters
  385. // Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may)
  386. .Where(p => p.RefKind == RefKind.None)
  387. // Attempt to determine the variant type
  388. .Select(p => MarshalUtils.ConvertManagedTypeToVariantType(p.Type, typeCache))
  389. // Discard parameter types that couldn't be determined (null entries)
  390. .Where(t => t != null).Cast<MarshalType>().ToImmutableArray();
  391. // If any parameter type was incompatible, it was discarded so the length won't match
  392. if (parameters.Length > paramTypes.Length)
  393. continue; // Ignore incompatible method
  394. yield return new GodotMethodInfo(method, paramTypes, parameters
  395. .Select(p => p.Type).ToImmutableArray(), retType);
  396. }
  397. }
  398. private static IEnumerable<GodotPropertyInfo> WhereIsCompatibleGodotType(
  399. IEnumerable<IPropertySymbol> properties,
  400. MarshalUtils.TypeCache typeCache
  401. )
  402. {
  403. foreach (var property in properties)
  404. {
  405. // Ignore properties without a getter. Godot properties must be readable.
  406. if (property.IsWriteOnly)
  407. continue;
  408. var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(property.Type, typeCache);
  409. if (marshalType == null)
  410. continue;
  411. yield return new GodotPropertyInfo(property, marshalType.Value);
  412. }
  413. }
  414. private static IEnumerable<GodotFieldInfo> WhereIsCompatibleGodotType(
  415. IEnumerable<IFieldSymbol> fields,
  416. MarshalUtils.TypeCache typeCache
  417. )
  418. {
  419. foreach (var field in fields)
  420. {
  421. var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(field.Type, typeCache);
  422. if (marshalType == null)
  423. continue;
  424. yield return new GodotFieldInfo(field, marshalType.Value);
  425. }
  426. }
  427. }
  428. }