mirror of
				https://github.com/godotengine/godot.git
				synced 2025-11-04 07:31:16 +00:00 
			
		
		
		
	The following two bugs were fixed:
- For classes without namespace we were still generating `namespace {`
without a namespace identifier, causing a syntax error.
- For classes with nested namespaces we were generating only the innermost
part of the namespace was being generated, e.g.: for `Foo.Bar` we were
generating `namespace Bar {` instead of `namespace Foo.Bar {`.
This wasn't causing any build error, but because of the wrong namespace
Godot wasn't able to find the class associated with the script.
		
	
			
		
			
				
	
	
		
			182 lines
		
	
	
	
		
			6.7 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			182 lines
		
	
	
	
		
			6.7 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
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)
 | 
						|
        {
 | 
						|
            if (context.TryGetGlobalAnalyzerProperty("GodotScriptPathAttributeGenerator", out string? toggle)
 | 
						|
                && toggle == "disabled")
 | 
						|
            {
 | 
						|
                return;
 | 
						|
            }
 | 
						|
 | 
						|
            // NOTE: IsNullOrEmpty doesn't work well with nullable checks
 | 
						|
            // ReSharper disable once ReplaceWithStringIsNullOrEmpty
 | 
						|
            if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir)
 | 
						|
                || godotProjectDir!.Length == 0)
 | 
						|
            {
 | 
						|
                throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
 | 
						|
            }
 | 
						|
 | 
						|
            var godotClasses = context.Compilation.SyntaxTrees
 | 
						|
                .SelectMany(tree =>
 | 
						|
                    tree.GetRoot().DescendantNodes()
 | 
						|
                        .OfType<ClassDeclarationSyntax>()
 | 
						|
                        // Ignore inner classes
 | 
						|
                        .Where(cds => !(cds.Parent is ClassDeclarationSyntax))
 | 
						|
                        .SelectGodotScriptClasses(context.Compilation)
 | 
						|
                        // Report and skip non-partial classes
 | 
						|
                        .Where(x =>
 | 
						|
                        {
 | 
						|
                            if (x.cds.IsPartial() || x.symbol.HasDisableGeneratorsAttribute())
 | 
						|
                                return true;
 | 
						|
                            Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
 | 
						|
                            return false;
 | 
						|
                        })
 | 
						|
                )
 | 
						|
                // Ignore classes whose name is not the same as the file name
 | 
						|
                .Where(x => Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name)
 | 
						|
                .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
 | 
						|
        )
 | 
						|
        {
 | 
						|
            var attributes = new StringBuilder();
 | 
						|
 | 
						|
            // 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);
 | 
						|
 | 
						|
                if (attributes.Length != 0)
 | 
						|
                    attributes.Append("\n");
 | 
						|
 | 
						|
                attributes.Append(@"[ScriptPathAttribute(""res://");
 | 
						|
                attributes.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir));
 | 
						|
                attributes.Append(@""")]");
 | 
						|
            }
 | 
						|
 | 
						|
            string className = symbol.Name;
 | 
						|
 | 
						|
            INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
 | 
						|
            string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
 | 
						|
                namespaceSymbol.FullQualifiedName() :
 | 
						|
                string.Empty;
 | 
						|
            bool hasNamespace = classNs.Length != 0;
 | 
						|
 | 
						|
            string uniqueName = hasNamespace ?
 | 
						|
                classNs + "." + className + "_ScriptPath_Generated" :
 | 
						|
                className + "_ScriptPath_Generated";
 | 
						|
 | 
						|
            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);
 | 
						|
            source.Append("\n    partial class ");
 | 
						|
            source.Append(className);
 | 
						|
            source.Append("\n{\n}\n");
 | 
						|
 | 
						|
            if (hasNamespace)
 | 
						|
            {
 | 
						|
                source.Append("\n}\n");
 | 
						|
            }
 | 
						|
 | 
						|
            context.AddSource(uniqueName, SourceText.From(source.ToString(), Encoding.UTF8));
 | 
						|
        }
 | 
						|
 | 
						|
        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(
 | 
						|
                    NullableFlowState.NotNull, SymbolDisplayFormat.FullyQualifiedFormat);
 | 
						|
                if (!first)
 | 
						|
                    sourceBuilder.Append(", ");
 | 
						|
                first = false;
 | 
						|
                sourceBuilder.Append("typeof(");
 | 
						|
                sourceBuilder.Append(qualifiedName);
 | 
						|
                sourceBuilder.Append(")");
 | 
						|
            }
 | 
						|
 | 
						|
            sourceBuilder.Append("})]\n");
 | 
						|
 | 
						|
            context.AddSource("AssemblyScriptTypes_Generated",
 | 
						|
                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());
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |