ScriptPathAttributeGenerator.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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.TryGetGlobalAnalyzerProperty("GodotScriptPathAttributeGenerator", out string? toggle)
  17. && toggle == "disabled")
  18. {
  19. return;
  20. }
  21. // NOTE: IsNullOrEmpty doesn't work well with nullable checks
  22. // ReSharper disable once ReplaceWithStringIsNullOrEmpty
  23. if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir)
  24. || godotProjectDir!.Length == 0)
  25. {
  26. throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
  27. }
  28. var godotClasses = context.Compilation.SyntaxTrees
  29. .SelectMany(tree =>
  30. tree.GetRoot().DescendantNodes()
  31. .OfType<ClassDeclarationSyntax>()
  32. // Ignore inner classes
  33. .Where(cds => !(cds.Parent is ClassDeclarationSyntax))
  34. .SelectGodotScriptClasses(context.Compilation)
  35. // Report and skip non-partial classes
  36. .Where(x =>
  37. {
  38. if (x.cds.IsPartial() || x.symbol.HasDisableGeneratorsAttribute())
  39. return true;
  40. Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
  41. return false;
  42. })
  43. )
  44. // Ignore classes whose name is not the same as the file name
  45. .Where(x => Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name)
  46. .GroupBy(x => x.symbol)
  47. .ToDictionary(g => g.Key, g => g.Select(x => x.cds));
  48. foreach (var godotClass in godotClasses)
  49. {
  50. VisitGodotScriptClass(context, godotProjectDir,
  51. symbol: godotClass.Key,
  52. classDeclarations: godotClass.Value);
  53. }
  54. if (godotClasses.Count <= 0)
  55. return;
  56. AddScriptTypesAssemblyAttr(context, godotClasses);
  57. }
  58. private static void VisitGodotScriptClass(
  59. GeneratorExecutionContext context,
  60. string godotProjectDir,
  61. INamedTypeSymbol symbol,
  62. IEnumerable<ClassDeclarationSyntax> classDeclarations
  63. )
  64. {
  65. var attributesBuilder = new StringBuilder();
  66. // Remember syntax trees for which we already added an attribute, to prevent unnecessary duplicates.
  67. var attributedTrees = new List<SyntaxTree>();
  68. foreach (var cds in classDeclarations)
  69. {
  70. if (attributedTrees.Contains(cds.SyntaxTree))
  71. continue;
  72. attributedTrees.Add(cds.SyntaxTree);
  73. if (attributesBuilder.Length != 0)
  74. attributesBuilder.Append("\n ");
  75. attributesBuilder.Append(@"[ScriptPathAttribute(""res://");
  76. attributesBuilder.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir));
  77. attributesBuilder.Append(@""")]");
  78. }
  79. string classNs = symbol.ContainingNamespace.Name;
  80. string className = symbol.Name;
  81. var source = $@"using Godot;
  82. namespace {classNs}
  83. {{
  84. {attributesBuilder}
  85. partial class {className}
  86. {{
  87. }}
  88. }}
  89. ";
  90. context.AddSource(classNs + "." + className + "_ScriptPath_Generated",
  91. SourceText.From(source, Encoding.UTF8));
  92. }
  93. private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context,
  94. Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses)
  95. {
  96. var sourceBuilder = new StringBuilder();
  97. sourceBuilder.Append("[assembly:");
  98. sourceBuilder.Append(GodotClasses.AssemblyHasScriptsAttr);
  99. sourceBuilder.Append("(new System.Type[] {");
  100. bool first = true;
  101. foreach (var godotClass in godotClasses)
  102. {
  103. var qualifiedName = godotClass.Key.ToDisplayString(
  104. NullableFlowState.NotNull, SymbolDisplayFormat.FullyQualifiedFormat);
  105. if (!first)
  106. sourceBuilder.Append(", ");
  107. first = false;
  108. sourceBuilder.Append("typeof(");
  109. sourceBuilder.Append(qualifiedName);
  110. sourceBuilder.Append(")");
  111. }
  112. sourceBuilder.Append("})]\n");
  113. context.AddSource("AssemblyScriptTypes_Generated",
  114. SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
  115. }
  116. public void Initialize(GeneratorInitializationContext context)
  117. {
  118. }
  119. private static string RelativeToDir(string path, string dir)
  120. {
  121. // Make sure the directory ends with a path separator
  122. dir = Path.Combine(dir, " ").TrimEnd();
  123. if (Path.DirectorySeparatorChar == '\\')
  124. dir = dir.Replace("/", "\\") + "\\";
  125. var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute);
  126. var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute);
  127. // MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString
  128. return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString());
  129. }
  130. }
  131. }