ScriptPropertiesGenerator.cs 25 KB

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