ScriptPropertiesGenerator.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. using System.Linq;
  2. using System.Text;
  3. using Microsoft.CodeAnalysis;
  4. using Microsoft.CodeAnalysis.CSharp.Syntax;
  5. using Microsoft.CodeAnalysis.Text;
  6. namespace Godot.SourceGenerators
  7. {
  8. [Generator]
  9. public class ScriptPropertiesGenerator : ISourceGenerator
  10. {
  11. public void Initialize(GeneratorInitializationContext context)
  12. {
  13. }
  14. public void Execute(GeneratorExecutionContext context)
  15. {
  16. if (context.AreGodotSourceGeneratorsDisabled())
  17. return;
  18. INamedTypeSymbol[] godotClasses = context
  19. .Compilation.SyntaxTrees
  20. .SelectMany(tree =>
  21. tree.GetRoot().DescendantNodes()
  22. .OfType<ClassDeclarationSyntax>()
  23. .SelectGodotScriptClasses(context.Compilation)
  24. // Report and skip non-partial classes
  25. .Where(x =>
  26. {
  27. if (x.cds.IsPartial())
  28. {
  29. if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
  30. {
  31. Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
  32. return false;
  33. }
  34. return true;
  35. }
  36. Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
  37. return false;
  38. })
  39. .Select(x => x.symbol)
  40. )
  41. .Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
  42. .ToArray();
  43. if (godotClasses.Length > 0)
  44. {
  45. var typeCache = new MarshalUtils.TypeCache(context);
  46. foreach (var godotClass in godotClasses)
  47. {
  48. VisitGodotScriptClass(context, typeCache, godotClass);
  49. }
  50. }
  51. }
  52. private static void VisitGodotScriptClass(
  53. GeneratorExecutionContext context,
  54. MarshalUtils.TypeCache typeCache,
  55. INamedTypeSymbol symbol
  56. )
  57. {
  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 uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint()
  65. + "_ScriptProperties_Generated";
  66. var source = new StringBuilder();
  67. source.Append("using Godot;\n");
  68. source.Append("using Godot.NativeInterop;\n");
  69. source.Append("\n");
  70. if (hasNamespace)
  71. {
  72. source.Append("namespace ");
  73. source.Append(classNs);
  74. source.Append(" {\n\n");
  75. }
  76. if (isInnerClass)
  77. {
  78. var containingType = symbol.ContainingType;
  79. while (containingType != null)
  80. {
  81. source.Append("partial ");
  82. source.Append(containingType.GetDeclarationKeyword());
  83. source.Append(" ");
  84. source.Append(containingType.NameWithTypeParameters());
  85. source.Append("\n{\n");
  86. containingType = containingType.ContainingType;
  87. }
  88. }
  89. source.Append("partial class ");
  90. source.Append(symbol.NameWithTypeParameters());
  91. source.Append("\n{\n");
  92. var members = symbol.GetMembers();
  93. var propertySymbols = members
  94. .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property)
  95. .Cast<IPropertySymbol>();
  96. var fieldSymbols = members
  97. .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared)
  98. .Cast<IFieldSymbol>();
  99. var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray();
  100. var godotClassFields = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray();
  101. source.Append(" private partial class GodotInternal {\n");
  102. // Generate cached StringNames for methods and properties, for fast lookup
  103. foreach (var property in godotClassProperties)
  104. {
  105. string propertyName = property.PropertySymbol.Name;
  106. source.Append(" public static readonly StringName PropName_");
  107. source.Append(propertyName);
  108. source.Append(" = \"");
  109. source.Append(propertyName);
  110. source.Append("\";\n");
  111. }
  112. foreach (var field in godotClassFields)
  113. {
  114. string fieldName = field.FieldSymbol.Name;
  115. source.Append(" public static readonly StringName PropName_");
  116. source.Append(fieldName);
  117. source.Append(" = \"");
  118. source.Append(fieldName);
  119. source.Append("\";\n");
  120. }
  121. source.Append(" }\n"); // class GodotInternal
  122. if (godotClassProperties.Length > 0 || godotClassFields.Length > 0)
  123. {
  124. bool isFirstEntry;
  125. // Generate SetGodotClassPropertyValue
  126. bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.FieldSymbol.IsReadOnly) &&
  127. godotClassProperties.All(pi => pi.PropertySymbol.IsReadOnly);
  128. if (!allPropertiesAreReadOnly)
  129. {
  130. source.Append(" protected override bool SetGodotClassPropertyValue(in godot_string_name name, ");
  131. source.Append("in godot_variant value)\n {\n");
  132. isFirstEntry = true;
  133. foreach (var property in godotClassProperties)
  134. {
  135. if (property.PropertySymbol.IsReadOnly)
  136. continue;
  137. GeneratePropertySetter(property.PropertySymbol.Name,
  138. property.PropertySymbol.Type, property.Type, source, isFirstEntry);
  139. isFirstEntry = false;
  140. }
  141. foreach (var field in godotClassFields)
  142. {
  143. if (field.FieldSymbol.IsReadOnly)
  144. continue;
  145. GeneratePropertySetter(field.FieldSymbol.Name,
  146. field.FieldSymbol.Type, field.Type, source, isFirstEntry);
  147. isFirstEntry = false;
  148. }
  149. source.Append(" return base.SetGodotClassPropertyValue(name, value);\n");
  150. source.Append(" }\n");
  151. }
  152. // Generate GetGodotClassPropertyValue
  153. source.Append(" protected override bool GetGodotClassPropertyValue(in godot_string_name name, ");
  154. source.Append("out godot_variant value)\n {\n");
  155. isFirstEntry = true;
  156. foreach (var property in godotClassProperties)
  157. {
  158. GeneratePropertyGetter(property.PropertySymbol.Name,
  159. property.Type, source, isFirstEntry);
  160. isFirstEntry = false;
  161. }
  162. foreach (var field in godotClassFields)
  163. {
  164. GeneratePropertyGetter(field.FieldSymbol.Name,
  165. field.Type, source, isFirstEntry);
  166. isFirstEntry = false;
  167. }
  168. source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n");
  169. source.Append(" }\n");
  170. // Generate GetGodotPropertyList
  171. source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");
  172. string dictionaryType = "System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>";
  173. source.Append(" internal new static ")
  174. .Append(dictionaryType)
  175. .Append(" GetGodotPropertyList()\n {\n");
  176. source.Append(" var properties = new ")
  177. .Append(dictionaryType)
  178. .Append("();\n");
  179. foreach (var property in godotClassProperties)
  180. {
  181. var propertyInfo = DeterminePropertyInfo(context, typeCache,
  182. property.PropertySymbol, property.Type);
  183. if (propertyInfo == null)
  184. continue;
  185. AppendPropertyInfo(source, propertyInfo.Value);
  186. }
  187. foreach (var field in godotClassFields)
  188. {
  189. var propertyInfo = DeterminePropertyInfo(context, typeCache,
  190. field.FieldSymbol, field.Type);
  191. if (propertyInfo == null)
  192. continue;
  193. AppendPropertyInfo(source, propertyInfo.Value);
  194. }
  195. source.Append(" return properties;\n");
  196. source.Append(" }\n");
  197. source.Append("#pragma warning restore CS0109\n");
  198. }
  199. source.Append("}\n"); // partial class
  200. if (isInnerClass)
  201. {
  202. var containingType = symbol.ContainingType;
  203. while (containingType != null)
  204. {
  205. source.Append("}\n"); // outer class
  206. containingType = containingType.ContainingType;
  207. }
  208. }
  209. if (hasNamespace)
  210. {
  211. source.Append("\n}\n");
  212. }
  213. context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
  214. }
  215. private static void GeneratePropertySetter(
  216. string propertyMemberName,
  217. ITypeSymbol propertyTypeSymbol,
  218. MarshalType propertyMarshalType,
  219. StringBuilder source,
  220. bool isFirstEntry
  221. )
  222. {
  223. source.Append(" ");
  224. if (!isFirstEntry)
  225. source.Append("else ");
  226. source.Append("if (name == GodotInternal.PropName_")
  227. .Append(propertyMemberName)
  228. .Append(") {\n")
  229. .Append(" ")
  230. .Append(propertyMemberName)
  231. .Append(" = ")
  232. .AppendNativeVariantToManagedExpr("value", propertyTypeSymbol, propertyMarshalType)
  233. .Append(";\n")
  234. .Append(" return true;\n")
  235. .Append(" }\n");
  236. }
  237. private static void GeneratePropertyGetter(
  238. string propertyMemberName,
  239. MarshalType propertyMarshalType,
  240. StringBuilder source,
  241. bool isFirstEntry
  242. )
  243. {
  244. source.Append(" ");
  245. if (!isFirstEntry)
  246. source.Append("else ");
  247. source.Append("if (name == GodotInternal.PropName_")
  248. .Append(propertyMemberName)
  249. .Append(") {\n")
  250. .Append(" value = ")
  251. .AppendManagedToNativeVariantExpr(propertyMemberName, propertyMarshalType)
  252. .Append(";\n")
  253. .Append(" return true;\n")
  254. .Append(" }\n");
  255. }
  256. private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo)
  257. {
  258. source.Append(" properties.Add(new(type: (Godot.Variant.Type)")
  259. .Append((int)propertyInfo.Type)
  260. .Append(", name: GodotInternal.PropName_")
  261. .Append(propertyInfo.Name)
  262. .Append(", hint: (Godot.PropertyHint)")
  263. .Append((int)propertyInfo.Hint)
  264. .Append(", hintString: \"")
  265. .Append(propertyInfo.HintString)
  266. .Append("\", usage: (Godot.PropertyUsageFlags)")
  267. .Append((int)propertyInfo.Usage)
  268. .Append(", exported: ")
  269. .Append(propertyInfo.Exported ? "true" : "false")
  270. .Append("));\n");
  271. }
  272. private static PropertyInfo? DeterminePropertyInfo(
  273. GeneratorExecutionContext context,
  274. MarshalUtils.TypeCache typeCache,
  275. ISymbol memberSymbol,
  276. MarshalType marshalType
  277. )
  278. {
  279. var exportAttr = memberSymbol.GetAttributes()
  280. .FirstOrDefault(a => a.AttributeClass?.IsGodotExportAttribute() ?? false);
  281. var propertySymbol = memberSymbol as IPropertySymbol;
  282. var fieldSymbol = memberSymbol as IFieldSymbol;
  283. if (exportAttr != null && propertySymbol != null)
  284. {
  285. if (propertySymbol.GetMethod == null)
  286. {
  287. // This should never happen, as we filtered WriteOnly properties, but just in case.
  288. Common.ReportExportedMemberIsWriteOnly(context, propertySymbol);
  289. return null;
  290. }
  291. if (propertySymbol.SetMethod == null)
  292. {
  293. // This should never happen, as we filtered ReadOnly properties, but just in case.
  294. Common.ReportExportedMemberIsReadOnly(context, propertySymbol);
  295. return null;
  296. }
  297. }
  298. var memberType = propertySymbol?.Type ?? fieldSymbol!.Type;
  299. var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
  300. string memberName = memberSymbol.Name;
  301. if (exportAttr == null)
  302. {
  303. return new PropertyInfo(memberVariantType, memberName, PropertyHint.None,
  304. hintString: null, PropertyUsageFlags.ScriptVariable, exported: false);
  305. }
  306. if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType,
  307. isTypeArgument: false, out var hint, out var hintString))
  308. {
  309. var constructorArguments = exportAttr.ConstructorArguments;
  310. if (constructorArguments.Length > 0)
  311. {
  312. var hintValue = exportAttr.ConstructorArguments[0].Value;
  313. hint = hintValue switch
  314. {
  315. null => PropertyHint.None,
  316. int intValue => (PropertyHint)intValue,
  317. _ => (PropertyHint)(long)hintValue
  318. };
  319. hintString = constructorArguments.Length > 1 ?
  320. exportAttr.ConstructorArguments[1].Value?.ToString() :
  321. null;
  322. }
  323. else
  324. {
  325. hint = PropertyHint.None;
  326. }
  327. }
  328. var propUsage = PropertyUsageFlags.Default | PropertyUsageFlags.ScriptVariable;
  329. if (memberVariantType == VariantType.Nil)
  330. propUsage |= PropertyUsageFlags.NilIsVariant;
  331. return new PropertyInfo(memberVariantType, memberName,
  332. hint, hintString, propUsage, exported: true);
  333. }
  334. private static bool TryGetMemberExportHint(
  335. MarshalUtils.TypeCache typeCache,
  336. ITypeSymbol type, AttributeData exportAttr,
  337. VariantType variantType, bool isTypeArgument,
  338. out PropertyHint hint, out string? hintString
  339. )
  340. {
  341. hint = PropertyHint.None;
  342. hintString = null;
  343. if (variantType == VariantType.Nil)
  344. return true; // Variant, no export hint
  345. if (variantType == VariantType.Int &&
  346. type.IsValueType && type.TypeKind == TypeKind.Enum)
  347. {
  348. bool hasFlagsAttr = type.GetAttributes()
  349. .Any(a => a.AttributeClass?.IsSystemFlagsAttribute() ?? false);
  350. hint = hasFlagsAttr ? PropertyHint.Flags : PropertyHint.Enum;
  351. var members = type.GetMembers();
  352. var enumFields = members
  353. .Where(s => s.Kind == SymbolKind.Field && s.IsStatic &&
  354. s.DeclaredAccessibility == Accessibility.Public &&
  355. !s.IsImplicitlyDeclared)
  356. .Cast<IFieldSymbol>().ToArray();
  357. var hintStringBuilder = new StringBuilder();
  358. var nameOnlyHintStringBuilder = new StringBuilder();
  359. // True: enum Foo { Bar, Baz, Qux }
  360. // True: enum Foo { Bar = 0, Baz = 1, Qux = 2 }
  361. // False: enum Foo { Bar = 0, Baz = 7, Qux = 5 }
  362. bool usesDefaultValues = true;
  363. for (int i = 0; i < enumFields.Length; i++)
  364. {
  365. var enumField = enumFields[i];
  366. if (i > 0)
  367. {
  368. hintStringBuilder.Append(",");
  369. nameOnlyHintStringBuilder.Append(",");
  370. }
  371. string enumFieldName = enumField.Name;
  372. hintStringBuilder.Append(enumFieldName);
  373. nameOnlyHintStringBuilder.Append(enumFieldName);
  374. long val = enumField.ConstantValue switch
  375. {
  376. sbyte v => v,
  377. short v => v,
  378. int v => v,
  379. long v => v,
  380. byte v => v,
  381. ushort v => v,
  382. uint v => v,
  383. ulong v => (long)v,
  384. _ => 0
  385. };
  386. uint expectedVal = (uint)(hint == PropertyHint.Flags ? 1 << i : i);
  387. if (val != expectedVal)
  388. usesDefaultValues = false;
  389. hintStringBuilder.Append(":");
  390. hintStringBuilder.Append(val);
  391. }
  392. hintString = !usesDefaultValues ?
  393. hintStringBuilder.ToString() :
  394. // If we use the format NAME:VAL, that's what the editor displays.
  395. // That's annoying if the user is not using custom values for the enum constants.
  396. // This may not be needed in the future if the editor is changed to not display values.
  397. nameOnlyHintStringBuilder.ToString();
  398. return true;
  399. }
  400. if (variantType == VariantType.Object && type is INamedTypeSymbol memberNamedType)
  401. {
  402. if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Resource"))
  403. {
  404. string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName()!;
  405. hint = PropertyHint.ResourceType;
  406. hintString = nativeTypeName;
  407. return true;
  408. }
  409. if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Node"))
  410. {
  411. string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName()!;
  412. hint = PropertyHint.NodeType;
  413. hintString = nativeTypeName;
  414. return true;
  415. }
  416. }
  417. static bool GetStringArrayEnumHint(VariantType elementVariantType,
  418. AttributeData exportAttr, out string? hintString)
  419. {
  420. var constructorArguments = exportAttr.ConstructorArguments;
  421. if (constructorArguments.Length > 0)
  422. {
  423. var presetHintValue = exportAttr.ConstructorArguments[0].Value;
  424. PropertyHint presetHint = presetHintValue switch
  425. {
  426. null => PropertyHint.None,
  427. int intValue => (PropertyHint)intValue,
  428. _ => (PropertyHint)(long)presetHintValue
  429. };
  430. if (presetHint == PropertyHint.Enum)
  431. {
  432. string? presetHintString = constructorArguments.Length > 1 ?
  433. exportAttr.ConstructorArguments[1].Value?.ToString() :
  434. null;
  435. hintString = (int)elementVariantType + "/" + (int)PropertyHint.Enum + ":";
  436. if (presetHintString != null)
  437. hintString += presetHintString;
  438. return true;
  439. }
  440. }
  441. hintString = null;
  442. return false;
  443. }
  444. if (!isTypeArgument && variantType == VariantType.Array)
  445. {
  446. var elementType = MarshalUtils.GetArrayElementType(type);
  447. if (elementType == null)
  448. return false; // Non-generic Array, so there's no hint to add
  449. var elementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementType, typeCache)!.Value;
  450. var elementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(elementMarshalType)!.Value;
  451. bool isPresetHint = false;
  452. if (elementVariantType == VariantType.String)
  453. isPresetHint = GetStringArrayEnumHint(elementVariantType, exportAttr, out hintString);
  454. if (!isPresetHint)
  455. {
  456. bool hintRes = TryGetMemberExportHint(typeCache, elementType,
  457. exportAttr, elementVariantType, isTypeArgument: true,
  458. out var elementHint, out var elementHintString);
  459. // Format: type/hint:hint_string
  460. if (hintRes)
  461. {
  462. hintString = (int)elementVariantType + "/" + (int)elementHint + ":";
  463. if (elementHintString != null)
  464. hintString += elementHintString;
  465. }
  466. else
  467. {
  468. hintString = (int)elementVariantType + "/" + (int)PropertyHint.None + ":";
  469. }
  470. }
  471. hint = PropertyHint.TypeString;
  472. return hintString != null;
  473. }
  474. if (!isTypeArgument && variantType == VariantType.PackedStringArray)
  475. {
  476. if (GetStringArrayEnumHint(VariantType.String, exportAttr, out hintString))
  477. {
  478. hint = PropertyHint.TypeString;
  479. return true;
  480. }
  481. }
  482. if (!isTypeArgument && variantType == VariantType.Dictionary)
  483. {
  484. // TODO: Dictionaries are not supported in the inspector
  485. return false;
  486. }
  487. return false;
  488. }
  489. }
  490. }