| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  | using System; | 
					
						
							|  |  |  | using System.Collections.Generic; | 
					
						
							|  |  |  | using System.IO; | 
					
						
							|  |  |  | using System.Linq; | 
					
						
							|  |  |  | using System.Text; | 
					
						
							|  |  |  | using Microsoft.CodeAnalysis; | 
					
						
							|  |  |  | using Microsoft.CodeAnalysis.CSharp.Syntax; | 
					
						
							|  |  |  | using Microsoft.CodeAnalysis.Text; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace Godot.SourceGenerators | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     [Generator] | 
					
						
							|  |  |  |     public class ScriptPathAttributeGenerator : ISourceGenerator | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         public void Execute(GeneratorExecutionContext context) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2021-12-28 23:25:16 +01:00
										 |  |  |             if (context.AreGodotSourceGeneratorsDisabled()) | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |                 return; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-28 23:25:16 +01:00
										 |  |  |             if (context.IsGodotToolsProject()) | 
					
						
							|  |  |  |                 return; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-28 23:25:16 +01:00
										 |  |  |             // NOTE: NotNullWhen diagnostics don't work on projects targeting .NET Standard 2.0 | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |             // ReSharper disable once ReplaceWithStringIsNullOrEmpty | 
					
						
							|  |  |  |             if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir) | 
					
						
							|  |  |  |                 || godotProjectDir!.Length == 0) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty."); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-28 23:25:16 +01:00
										 |  |  |             Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses = context | 
					
						
							|  |  |  |                 .Compilation.SyntaxTrees | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |                 .SelectMany(tree => | 
					
						
							|  |  |  |                     tree.GetRoot().DescendantNodes() | 
					
						
							|  |  |  |                         .OfType<ClassDeclarationSyntax>() | 
					
						
							|  |  |  |                         // Ignore inner classes | 
					
						
							| 
									
										
										
										
											2021-12-28 23:25:16 +01:00
										 |  |  |                         .Where(cds => !cds.IsNested()) | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |                         .SelectGodotScriptClasses(context.Compilation) | 
					
						
							|  |  |  |                         // Report and skip non-partial classes | 
					
						
							|  |  |  |                         .Where(x => | 
					
						
							|  |  |  |                         { | 
					
						
							| 
									
										
										
										
											2021-12-28 23:25:16 +01:00
										 |  |  |                             if (x.cds.IsPartial()) | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |                                 return true; | 
					
						
							|  |  |  |                             Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); | 
					
						
							|  |  |  |                             return false; | 
					
						
							|  |  |  |                         }) | 
					
						
							|  |  |  |                 ) | 
					
						
							| 
									
										
										
										
											2022-12-24 03:34:02 +01:00
										 |  |  |                 .Where(x => | 
					
						
							|  |  |  |                     // Ignore classes whose name is not the same as the file name | 
					
						
							|  |  |  |                     Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name && | 
					
						
							|  |  |  |                     // Ignore generic classes | 
					
						
							|  |  |  |                     !x.symbol.IsGenericType) | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |                 .GroupBy(x => x.symbol) | 
					
						
							|  |  |  |                 .ToDictionary(g => g.Key, g => g.Select(x => x.cds)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             foreach (var godotClass in godotClasses) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 VisitGodotScriptClass(context, godotProjectDir, | 
					
						
							|  |  |  |                     symbol: godotClass.Key, | 
					
						
							|  |  |  |                     classDeclarations: godotClass.Value); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (godotClasses.Count <= 0) | 
					
						
							|  |  |  |                 return; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             AddScriptTypesAssemblyAttr(context, godotClasses); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         private static void VisitGodotScriptClass( | 
					
						
							|  |  |  |             GeneratorExecutionContext context, | 
					
						
							|  |  |  |             string godotProjectDir, | 
					
						
							|  |  |  |             INamedTypeSymbol symbol, | 
					
						
							|  |  |  |             IEnumerable<ClassDeclarationSyntax> classDeclarations | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2021-03-13 01:04:55 +01:00
										 |  |  |             var attributes = new StringBuilder(); | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |             // Remember syntax trees for which we already added an attribute, to prevent unnecessary duplicates. | 
					
						
							|  |  |  |             var attributedTrees = new List<SyntaxTree>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             foreach (var cds in classDeclarations) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 if (attributedTrees.Contains(cds.SyntaxTree)) | 
					
						
							|  |  |  |                     continue; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 attributedTrees.Add(cds.SyntaxTree); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-13 01:04:55 +01:00
										 |  |  |                 if (attributes.Length != 0) | 
					
						
							|  |  |  |                     attributes.Append("\n"); | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-13 01:04:55 +01:00
										 |  |  |                 attributes.Append(@"[ScriptPathAttribute(""res://"); | 
					
						
							|  |  |  |                 attributes.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir)); | 
					
						
							|  |  |  |                 attributes.Append(@""")]"); | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-13 01:04:55 +01:00
										 |  |  |             INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; | 
					
						
							|  |  |  |             string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? | 
					
						
							| 
									
										
										
										
											2022-11-24 01:04:15 +01:00
										 |  |  |                 namespaceSymbol.FullQualifiedNameOmitGlobal() : | 
					
						
							| 
									
										
										
										
											2021-03-13 01:04:55 +01:00
										 |  |  |                 string.Empty; | 
					
						
							|  |  |  |             bool hasNamespace = classNs.Length != 0; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-24 01:04:15 +01:00
										 |  |  |             string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint() | 
					
						
							| 
									
										
										
										
											2022-10-22 23:13:52 +02:00
										 |  |  |                              + "_ScriptPath.generated"; | 
					
						
							| 
									
										
										
										
											2021-03-13 01:04:55 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |             var source = new StringBuilder(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // using Godot; | 
					
						
							|  |  |  |             // namespace {classNs} { | 
					
						
							|  |  |  |             //     {attributesBuilder} | 
					
						
							|  |  |  |             //     partial class {className} { } | 
					
						
							|  |  |  |             // } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             source.Append("using Godot;\n"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (hasNamespace) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 source.Append("namespace "); | 
					
						
							|  |  |  |                 source.Append(classNs); | 
					
						
							|  |  |  |                 source.Append(" {\n\n"); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             source.Append(attributes); | 
					
						
							| 
									
										
										
										
											2022-02-27 21:57:30 +01:00
										 |  |  |             source.Append("\npartial class "); | 
					
						
							|  |  |  |             source.Append(symbol.NameWithTypeParameters()); | 
					
						
							| 
									
										
										
										
											2021-03-13 01:04:55 +01:00
										 |  |  |             source.Append("\n{\n}\n"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (hasNamespace) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 source.Append("\n}\n"); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-22 23:13:52 +02:00
										 |  |  |             context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context, | 
					
						
							|  |  |  |             Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             var sourceBuilder = new StringBuilder(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             sourceBuilder.Append("[assembly:"); | 
					
						
							|  |  |  |             sourceBuilder.Append(GodotClasses.AssemblyHasScriptsAttr); | 
					
						
							|  |  |  |             sourceBuilder.Append("(new System.Type[] {"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             bool first = true; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             foreach (var godotClass in godotClasses) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 var qualifiedName = godotClass.Key.ToDisplayString( | 
					
						
							| 
									
										
										
										
											2021-12-03 18:40:32 +01:00
										 |  |  |                     NullableFlowState.NotNull, SymbolDisplayFormat.FullyQualifiedFormat | 
					
						
							|  |  |  |                         .WithGenericsOptions(SymbolDisplayGenericsOptions.None)); | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |                 if (!first) | 
					
						
							|  |  |  |                     sourceBuilder.Append(", "); | 
					
						
							|  |  |  |                 first = false; | 
					
						
							|  |  |  |                 sourceBuilder.Append("typeof("); | 
					
						
							|  |  |  |                 sourceBuilder.Append(qualifiedName); | 
					
						
							|  |  |  |                 sourceBuilder.Append(")"); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             sourceBuilder.Append("})]\n"); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-22 23:13:52 +02:00
										 |  |  |             context.AddSource("AssemblyScriptTypes.generated", | 
					
						
							| 
									
										
										
										
											2021-03-06 00:12:42 +01:00
										 |  |  |                 SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         public void Initialize(GeneratorInitializationContext context) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         private static string RelativeToDir(string path, string dir) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             // Make sure the directory ends with a path separator | 
					
						
							|  |  |  |             dir = Path.Combine(dir, " ").TrimEnd(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (Path.DirectorySeparatorChar == '\\') | 
					
						
							|  |  |  |                 dir = dir.Replace("/", "\\") + "\\"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute); | 
					
						
							|  |  |  |             var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString | 
					
						
							|  |  |  |             return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString()); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |