ScriptPathAttributeGenerator.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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.IsGodotSourceGeneratorDisabled("ScriptPathAttribute"))
  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("GodotProjectDirBase64", out string? godotProjectDir) || godotProjectDir!.Length == 0)
  23. {
  24. if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out godotProjectDir) || godotProjectDir!.Length == 0)
  25. {
  26. throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
  27. }
  28. }
  29. else
  30. {
  31. // Workaround for https://github.com/dotnet/roslyn/issues/51692
  32. godotProjectDir = Encoding.UTF8.GetString(Convert.FromBase64String(godotProjectDir));
  33. }
  34. Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses = context
  35. .Compilation.SyntaxTrees
  36. .SelectMany(tree =>
  37. tree.GetRoot().DescendantNodes()
  38. .OfType<ClassDeclarationSyntax>()
  39. // Ignore inner classes
  40. .Where(cds => !cds.IsNested())
  41. .SelectGodotScriptClasses(context.Compilation)
  42. // Report and skip non-partial classes
  43. .Where(x =>
  44. {
  45. if (x.cds.IsPartial())
  46. return true;
  47. Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
  48. return false;
  49. })
  50. )
  51. .Where(x =>
  52. // Ignore classes whose name is not the same as the file name
  53. Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name)
  54. .GroupBy<(ClassDeclarationSyntax cds, INamedTypeSymbol symbol), INamedTypeSymbol>(x => x.symbol, SymbolEqualityComparer.Default)
  55. .ToDictionary<IGrouping<INamedTypeSymbol, (ClassDeclarationSyntax cds, INamedTypeSymbol symbol)>, INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>>(g => g.Key, g => g.Select(x => x.cds), SymbolEqualityComparer.Default);
  56. var usedPaths = new HashSet<string>();
  57. foreach (var godotClass in godotClasses)
  58. {
  59. VisitGodotScriptClass(context, godotProjectDir, usedPaths,
  60. symbol: godotClass.Key,
  61. classDeclarations: godotClass.Value);
  62. }
  63. if (godotClasses.Count <= 0)
  64. return;
  65. AddScriptTypesAssemblyAttr(context, godotClasses);
  66. }
  67. private static void VisitGodotScriptClass(
  68. GeneratorExecutionContext context,
  69. string godotProjectDir,
  70. HashSet<string> usedPaths,
  71. INamedTypeSymbol symbol,
  72. IEnumerable<ClassDeclarationSyntax> classDeclarations
  73. )
  74. {
  75. var attributes = new StringBuilder();
  76. // Remember syntax trees for which we already added an attribute, to prevent unnecessary duplicates.
  77. var attributedTrees = new List<SyntaxTree>();
  78. foreach (var cds in classDeclarations)
  79. {
  80. if (attributedTrees.Contains(cds.SyntaxTree))
  81. continue;
  82. attributedTrees.Add(cds.SyntaxTree);
  83. if (attributes.Length != 0)
  84. attributes.Append("\n");
  85. string scriptPath = RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir);
  86. if (!usedPaths.Add(scriptPath))
  87. {
  88. context.ReportDiagnostic(Diagnostic.Create(
  89. Common.MultipleClassesInGodotScriptRule,
  90. cds.Identifier.GetLocation(),
  91. symbol.Name
  92. ));
  93. return;
  94. }
  95. attributes.Append(@"[ScriptPathAttribute(""res://");
  96. attributes.Append(scriptPath);
  97. attributes.Append(@""")]");
  98. }
  99. INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
  100. string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
  101. namespaceSymbol.FullQualifiedNameOmitGlobal() :
  102. string.Empty;
  103. bool hasNamespace = classNs.Length != 0;
  104. string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
  105. + "_ScriptPath.generated";
  106. var source = new StringBuilder();
  107. // using Godot;
  108. // namespace {classNs} {
  109. // {attributesBuilder}
  110. // partial class {className} { }
  111. // }
  112. source.Append("using Godot;\n");
  113. if (hasNamespace)
  114. {
  115. source.Append("namespace ");
  116. source.Append(classNs);
  117. source.Append(" {\n\n");
  118. }
  119. source.Append(attributes);
  120. source.Append("\npartial class ");
  121. source.Append(symbol.NameWithTypeParameters());
  122. source.Append("\n{\n}\n");
  123. if (hasNamespace)
  124. {
  125. source.Append("\n}\n");
  126. }
  127. context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
  128. }
  129. private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context,
  130. Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses)
  131. {
  132. var sourceBuilder = new StringBuilder();
  133. sourceBuilder.Append("[assembly:");
  134. sourceBuilder.Append(GodotClasses.AssemblyHasScriptsAttr);
  135. sourceBuilder.Append("(new System.Type[] {");
  136. bool first = true;
  137. foreach (var godotClass in godotClasses)
  138. {
  139. var qualifiedName = godotClass.Key.ToDisplayString(
  140. NullableFlowState.NotNull, SymbolDisplayFormat.FullyQualifiedFormat
  141. .WithGenericsOptions(SymbolDisplayGenericsOptions.None));
  142. if (!first)
  143. sourceBuilder.Append(", ");
  144. first = false;
  145. sourceBuilder.Append("typeof(");
  146. sourceBuilder.Append(qualifiedName);
  147. if (godotClass.Key.IsGenericType)
  148. sourceBuilder.Append($"<{new string(',', godotClass.Key.TypeParameters.Count() - 1)}>");
  149. sourceBuilder.Append(")");
  150. }
  151. sourceBuilder.Append("})]\n");
  152. context.AddSource("AssemblyScriptTypes.generated",
  153. SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
  154. }
  155. public void Initialize(GeneratorInitializationContext context)
  156. {
  157. }
  158. private static string RelativeToDir(string path, string dir)
  159. {
  160. // Make sure the directory ends with a path separator
  161. dir = Path.Combine(dir, " ").TrimEnd();
  162. if (Path.DirectorySeparatorChar == '\\')
  163. dir = dir.Replace("/", "\\") + "\\";
  164. var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute);
  165. var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute);
  166. // MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString
  167. return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString());
  168. }
  169. }
  170. }