ScriptPathAttributeGenerator.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using Microsoft.CodeAnalysis;
  7. using Microsoft.CodeAnalysis.CSharp.Syntax;
  8. using Microsoft.CodeAnalysis.Text;
  9. namespace Godot.SourceGenerators
  10. {
  11. [Generator]
  12. public class ScriptPathAttributeGenerator : ISourceGenerator
  13. {
  14. public void Execute(GeneratorExecutionContext context)
  15. {
  16. if (context.AreGodotSourceGeneratorsDisabled())
  17. return;
  18. if (context.IsGodotToolsProject())
  19. return;
  20. // NOTE: NotNullWhen diagnostics don't work on projects targeting .NET Standard 2.0
  21. // ReSharper disable once ReplaceWithStringIsNullOrEmpty
  22. if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir)
  23. || godotProjectDir!.Length == 0)
  24. {
  25. throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
  26. }
  27. Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses = context
  28. .Compilation.SyntaxTrees
  29. .SelectMany(tree =>
  30. tree.GetRoot().DescendantNodes()
  31. .OfType<ClassDeclarationSyntax>()
  32. // Ignore inner classes
  33. .Where(cds => !cds.IsNested())
  34. .SelectGodotScriptClasses(context.Compilation)
  35. // Report and skip non-partial classes
  36. .Where(x =>
  37. {
  38. if (x.cds.IsPartial())
  39. return true;
  40. Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
  41. return false;
  42. })
  43. )
  44. .Where(x =>
  45. // Ignore classes whose name is not the same as the file name
  46. Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name &&
  47. // Ignore generic classes
  48. !x.symbol.IsGenericType)
  49. .GroupBy(x => x.symbol)
  50. .ToDictionary(g => g.Key, g => g.Select(x => x.cds));
  51. foreach (var godotClass in godotClasses)
  52. {
  53. VisitGodotScriptClass(context, godotProjectDir,
  54. symbol: godotClass.Key,
  55. classDeclarations: godotClass.Value);
  56. }
  57. if (godotClasses.Count <= 0)
  58. return;
  59. AddScriptTypesAssemblyAttr(context, godotClasses);
  60. }
  61. private static void VisitGodotScriptClass(
  62. GeneratorExecutionContext context,
  63. string godotProjectDir,
  64. INamedTypeSymbol symbol,
  65. IEnumerable<ClassDeclarationSyntax> classDeclarations
  66. )
  67. {
  68. var attributes = new StringBuilder();
  69. // Remember syntax trees for which we already added an attribute, to prevent unnecessary duplicates.
  70. var attributedTrees = new List<SyntaxTree>();
  71. foreach (var cds in classDeclarations)
  72. {
  73. if (attributedTrees.Contains(cds.SyntaxTree))
  74. continue;
  75. attributedTrees.Add(cds.SyntaxTree);
  76. if (attributes.Length != 0)
  77. attributes.Append("\n");
  78. attributes.Append(@"[ScriptPathAttribute(""res://");
  79. attributes.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir));
  80. attributes.Append(@""")]");
  81. }
  82. INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
  83. string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
  84. namespaceSymbol.FullQualifiedNameOmitGlobal() :
  85. string.Empty;
  86. bool hasNamespace = classNs.Length != 0;
  87. string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
  88. + "_ScriptPath.generated";
  89. var source = new StringBuilder();
  90. // using Godot;
  91. // namespace {classNs} {
  92. // {attributesBuilder}
  93. // partial class {className} { }
  94. // }
  95. source.Append("using Godot;\n");
  96. if (hasNamespace)
  97. {
  98. source.Append("namespace ");
  99. source.Append(classNs);
  100. source.Append(" {\n\n");
  101. }
  102. source.Append(attributes);
  103. source.Append("\npartial class ");
  104. source.Append(symbol.NameWithTypeParameters());
  105. source.Append("\n{\n}\n");
  106. if (hasNamespace)
  107. {
  108. source.Append("\n}\n");
  109. }
  110. context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
  111. }
  112. private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context,
  113. Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses)
  114. {
  115. var sourceBuilder = new StringBuilder();
  116. sourceBuilder.Append("[assembly:");
  117. sourceBuilder.Append(GodotClasses.AssemblyHasScriptsAttr);
  118. sourceBuilder.Append("(new System.Type[] {");
  119. bool first = true;
  120. foreach (var godotClass in godotClasses)
  121. {
  122. var qualifiedName = godotClass.Key.ToDisplayString(
  123. NullableFlowState.NotNull, SymbolDisplayFormat.FullyQualifiedFormat
  124. .WithGenericsOptions(SymbolDisplayGenericsOptions.None));
  125. if (!first)
  126. sourceBuilder.Append(", ");
  127. first = false;
  128. sourceBuilder.Append("typeof(");
  129. sourceBuilder.Append(qualifiedName);
  130. sourceBuilder.Append(")");
  131. }
  132. sourceBuilder.Append("})]\n");
  133. context.AddSource("AssemblyScriptTypes.generated",
  134. SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
  135. }
  136. public void Initialize(GeneratorInitializationContext context)
  137. {
  138. }
  139. private static string RelativeToDir(string path, string dir)
  140. {
  141. // Make sure the directory ends with a path separator
  142. dir = Path.Combine(dir, " ").TrimEnd();
  143. if (Path.DirectorySeparatorChar == '\\')
  144. dir = dir.Replace("/", "\\") + "\\";
  145. var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute);
  146. var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute);
  147. // MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString
  148. return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString());
  149. }
  150. }
  151. }